From 6127a9a0418f7bf08f9eff874f3a76add5698fba Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 29 Jun 2022 12:01:38 -0600 Subject: [PATCH 001/820] Intellifire climate Entity (#70818) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/intellifire/__init__.py | 2 +- .../components/intellifire/climate.py | 120 ++++++++++++++++++ homeassistant/components/intellifire/const.py | 2 + .../components/intellifire/coordinator.py | 2 +- 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/intellifire/climate.py diff --git a/.coveragerc b/.coveragerc index f52631a57bd..2443e30a2c3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -555,6 +555,7 @@ omit = homeassistant/components/insteon/utils.py homeassistant/components/intellifire/__init__.py homeassistant/components/intellifire/coordinator.py + homeassistant/components/intellifire/climate.py homeassistant/components/intellifire/binary_sensor.py homeassistant/components/intellifire/sensor.py homeassistant/components/intellifire/switch.py diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 034e74c2aa6..5fb8ac92a72 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -20,7 +20,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import CONF_USER_ID, DOMAIN, LOGGER from .coordinator import IntellifireDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.CLIMATE, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py new file mode 100644 index 00000000000..37a126bf3d6 --- /dev/null +++ b/homeassistant/components/intellifire/climate.py @@ -0,0 +1,120 @@ +"""Intellifire Climate Entities.""" +from __future__ import annotations + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityDescription, + ClimateEntityFeature, +) +from homeassistant.components.climate.const import HVACMode +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import IntellifireDataUpdateCoordinator +from .const import DEFAULT_THERMOSTAT_TEMP, DOMAIN, LOGGER +from .entity import IntellifireEntity + +INTELLIFIRE_CLIMATES: tuple[ClimateEntityDescription, ...] = ( + ClimateEntityDescription(key="climate", name="Thermostat"), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Configure the fan entry..""" + coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if coordinator.data.has_thermostat: + async_add_entities( + IntellifireClimate( + coordinator=coordinator, + description=description, + ) + for description in INTELLIFIRE_CLIMATES + ) + + +class IntellifireClimate(IntellifireEntity, ClimateEntity): + """Intellifire climate entity.""" + + entity_description: ClimateEntityDescription + + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] + _attr_min_temp = 0 + _attr_max_temp = 37 + _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_target_temperature_step = 1.0 + _attr_temperature_unit = TEMP_CELSIUS + last_temp = DEFAULT_THERMOSTAT_TEMP + + def __init__( + self, + coordinator: IntellifireDataUpdateCoordinator, + description: ClimateEntityDescription, + ) -> None: + """Configure climate entry - and override last_temp if the thermostat is currently on.""" + super().__init__(coordinator, description) + + if coordinator.data.thermostat_on: + self.last_temp = coordinator.data.thermostat_setpoint_c + + @property + def hvac_mode(self) -> str: + """Return current hvac mode.""" + if self.coordinator.read_api.data.thermostat_on: + return HVACMode.HEAT + return HVACMode.OFF + + async def async_set_temperature(self, **kwargs) -> None: + """Turn on thermostat by setting a target temperature.""" + raw_target_temp = kwargs[ATTR_TEMPERATURE] + self.last_temp = int(raw_target_temp) + LOGGER.debug( + "Setting target temp to %sc %sf", + int(raw_target_temp), + (raw_target_temp * 9 / 5) + 32, + ) + await self.coordinator.control_api.set_thermostat_c( + fireplace=self.coordinator.control_api.default_fireplace, + temp_c=self.last_temp, + ) + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return float(self.coordinator.read_api.data.temperature_c) + + @property + def target_temperature(self) -> float: + """Return target temperature.""" + return float(self.coordinator.read_api.data.thermostat_setpoint_c) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set HVAC mode to normal or thermostat control.""" + LOGGER.debug( + "Setting mode to [%s] - using last temp: %s", hvac_mode, self.last_temp + ) + + if hvac_mode == HVACMode.OFF: + await self.coordinator.control_api.turn_off_thermostat( + fireplace=self.coordinator.control_api.default_fireplace + ) + return + + # hvac_mode == HVACMode.HEAT + # 1) Set the desired target temp + await self.coordinator.control_api.set_thermostat_c( + fireplace=self.coordinator.control_api.default_fireplace, + temp_c=self.last_temp, + ) + + # 2) Make sure the fireplace is on! + if not self.coordinator.read_api.data.is_on: + await self.coordinator.control_api.flame_on( + fireplace=self.coordinator.control_api.default_fireplace, + ) diff --git a/homeassistant/components/intellifire/const.py b/homeassistant/components/intellifire/const.py index 2e9a2fabc06..cae25ea11ae 100644 --- a/homeassistant/components/intellifire/const.py +++ b/homeassistant/components/intellifire/const.py @@ -10,3 +10,5 @@ CONF_USER_ID = "user_id" LOGGER = logging.getLogger(__package__) CONF_SERIAL = "serial" + +DEFAULT_THERMOSTAT_TEMP = 21 diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index 39f197285d4..356ddedf16d 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -45,7 +45,7 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData except (ConnectionError, ClientConnectionError) as exception: raise UpdateFailed from exception - LOGGER.info("Failure Count %d", self._api.failed_poll_attempts) + LOGGER.debug("Failure Count %d", self._api.failed_poll_attempts) if self._api.failed_poll_attempts > 10: LOGGER.debug("Too many polling errors - raising exception") raise UpdateFailed From fe68c15a4a4a703081c9b9b810b1f09608c6714c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Jun 2022 20:20:57 +0200 Subject: [PATCH 002/820] Bump version to 2022.8.0dev0 (#74184) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bf690740c6d..54a1997b28e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 10 PIP_CACHE_VERSION: 4 - HA_SHORT_VERSION: 2022.7 + HA_SHORT_VERSION: 2022.8 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache diff --git a/homeassistant/const.py b/homeassistant/const.py index 698f6bee240..10523aa6d53 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 7 +MINOR_VERSION: Final = 8 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index 59df3967b6e..5e5974d142f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0.dev0" +version = "2022.8.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e4bd53b39510062b6b340435cd2614bcfa80c158 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 29 Jun 2022 18:52:54 -0400 Subject: [PATCH 003/820] Fix duplicate key for motion sensor for UniFi Protect (#74202) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index d3bf71a4274..62a4893692b 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -150,7 +150,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( - key="motion", + key="motion_enabled", name="Detections: Motion", icon="mdi:run-fast", ufp_value="recording_settings.enable_motion_detection", From 0028dc46e652634fd28c53a584a75a11b4cb434e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 30 Jun 2022 02:10:25 +0300 Subject: [PATCH 004/820] Fix Shelly Duo RGBW color mode attribute (#74193) --- homeassistant/components/shelly/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 79db9c509f4..b75e1ad2377 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -215,7 +215,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self.mode == "color": - if hasattr(self.block, "white"): + if self.wrapper.model in RGBW_MODELS: return ColorMode.RGBW return ColorMode.RGB From 1555f40bad9cdd5198e70f28a8c08ceea6b3c703 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 29 Jun 2022 19:10:38 -0400 Subject: [PATCH 005/820] Add UniFi Protect views (#74190) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/__init__.py | 3 + .../components/unifiprotect/views.py | 211 +++++++++ tests/components/unifiprotect/conftest.py | 6 + tests/components/unifiprotect/test_views.py | 427 ++++++++++++++++++ 4 files changed, 647 insertions(+) create mode 100644 homeassistant/components/unifiprotect/views.py create mode 100644 tests/components/unifiprotect/test_views.py diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 40214b60766..5b4059ee1d4 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -40,6 +40,7 @@ from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services from .utils import _async_unifi_mac_from_hass, async_get_devices +from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) @@ -92,6 +93,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service hass.config_entries.async_setup_platforms(entry, PLATFORMS) async_setup_services(hass) + hass.http.register_view(ThumbnailProxyView(hass)) + hass.http.register_view(VideoProxyView(hass)) entry.async_on_unload(entry.add_update_listener(_async_options_updated)) entry.async_on_unload( diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py new file mode 100644 index 00000000000..ea523d36dd2 --- /dev/null +++ b/homeassistant/components/unifiprotect/views.py @@ -0,0 +1,211 @@ +"""UniFi Protect Integration views.""" +from __future__ import annotations + +from datetime import datetime +from http import HTTPStatus +import logging +from typing import Any +from urllib.parse import urlencode + +from aiohttp import web +from pyunifiprotect.data import Event +from pyunifiprotect.exceptions import ClientError + +from homeassistant.components.http import HomeAssistantView +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN +from .data import ProtectData + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_generate_thumbnail_url( + event_id: str, + nvr_id: str, + width: int | None = None, + height: int | None = None, +) -> str: + """Generate URL for event thumbnail.""" + + url_format = ThumbnailProxyView.url or "{nvr_id}/{event_id}" + url = url_format.format(nvr_id=nvr_id, event_id=event_id) + + params = {} + if width is not None: + params["width"] = str(width) + if height is not None: + params["height"] = str(height) + + return f"{url}?{urlencode(params)}" + + +@callback +def async_generate_event_video_url(event: Event) -> str: + """Generate URL for event video.""" + + _validate_event(event) + if event.start is None or event.end is None: + raise ValueError("Event is ongoing") + + url_format = VideoProxyView.url or "{nvr_id}/{camera_id}/{start}/{end}" + url = url_format.format( + nvr_id=event.api.bootstrap.nvr.id, + camera_id=event.camera_id, + start=event.start.isoformat(), + end=event.end.isoformat(), + ) + + return url + + +@callback +def _client_error(message: Any, code: HTTPStatus) -> web.Response: + _LOGGER.warning("Client error (%s): %s", code.value, message) + if code == HTTPStatus.BAD_REQUEST: + return web.Response(body=message, status=code) + return web.Response(status=code) + + +@callback +def _400(message: Any) -> web.Response: + return _client_error(message, HTTPStatus.BAD_REQUEST) + + +@callback +def _403(message: Any) -> web.Response: + return _client_error(message, HTTPStatus.FORBIDDEN) + + +@callback +def _404(message: Any) -> web.Response: + return _client_error(message, HTTPStatus.NOT_FOUND) + + +@callback +def _validate_event(event: Event) -> None: + if event.camera is None: + raise ValueError("Event does not have a camera") + if not event.camera.can_read_media(event.api.bootstrap.auth_user): + raise PermissionError(f"User cannot read media from camera: {event.camera.id}") + + +class ProtectProxyView(HomeAssistantView): + """Base class to proxy request to UniFi Protect console.""" + + requires_auth = True + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize a thumbnail proxy view.""" + self.hass = hass + self.data = hass.data[DOMAIN] + + def _get_data_or_404(self, nvr_id: str) -> ProtectData | web.Response: + all_data: list[ProtectData] = [] + + for data in self.data.values(): + if isinstance(data, ProtectData): + if data.api.bootstrap.nvr.id == nvr_id: + return data + all_data.append(data) + return _404("Invalid NVR ID") + + +class ThumbnailProxyView(ProtectProxyView): + """View to proxy event thumbnails from UniFi Protect.""" + + url = "/api/unifiprotect/thumbnail/{nvr_id}/{event_id}" + name = "api:unifiprotect_thumbnail" + + async def get( + self, request: web.Request, nvr_id: str, event_id: str + ) -> web.Response: + """Get Event Thumbnail.""" + + data = self._get_data_or_404(nvr_id) + if isinstance(data, web.Response): + return data + + width: int | str | None = request.query.get("width") + height: int | str | None = request.query.get("height") + + if width is not None: + try: + width = int(width) + except ValueError: + return _400("Invalid width param") + if height is not None: + try: + height = int(height) + except ValueError: + return _400("Invalid height param") + + try: + thumbnail = await data.api.get_event_thumbnail( + event_id, width=width, height=height + ) + except ClientError as err: + return _404(err) + + if thumbnail is None: + return _404("Event thumbnail not found") + + return web.Response(body=thumbnail, content_type="image/jpeg") + + +class VideoProxyView(ProtectProxyView): + """View to proxy video clips from UniFi Protect.""" + + url = "/api/unifiprotect/video/{nvr_id}/{camera_id}/{start}/{end}" + name = "api:unifiprotect_thumbnail" + + async def get( + self, request: web.Request, nvr_id: str, camera_id: str, start: str, end: str + ) -> web.StreamResponse: + """Get Camera Video clip.""" + + data = self._get_data_or_404(nvr_id) + if isinstance(data, web.Response): + return data + + camera = data.api.bootstrap.cameras.get(camera_id) + if camera is None: + return _404(f"Invalid camera ID: {camera_id}") + if not camera.can_read_media(data.api.bootstrap.auth_user): + return _403(f"User cannot read media from camera: {camera.id}") + + try: + start_dt = datetime.fromisoformat(start) + except ValueError: + return _400("Invalid start") + + try: + end_dt = datetime.fromisoformat(end) + except ValueError: + return _400("Invalid end") + + response = web.StreamResponse( + status=200, + reason="OK", + headers={ + "Content-Type": "video/mp4", + }, + ) + + async def iterator(total: int, chunk: bytes | None) -> None: + if not response.prepared: + response.content_length = total + await response.prepare(request) + + if chunk is not None: + await response.write(chunk) + + try: + await camera.get_video(start_dt, end_dt, iterator_callback=iterator) + except ClientError as err: + return _404(err) + + if response.prepared: + await response.write_eof() + return response diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 51cef190e2f..2a9edb605e7 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta +from functools import partial from ipaddress import IPv4Address import json from typing import Any @@ -102,6 +103,11 @@ def mock_ufp_client(bootstrap: Bootstrap): """Mock ProtectApiClient for testing.""" client = Mock() client.bootstrap = bootstrap + client._bootstrap = bootstrap + client.api_path = "/api" + # functionality from API client tests actually need + client._stream_response = partial(ProtectApiClient._stream_response, client) + client.get_camera_video = partial(ProtectApiClient.get_camera_video, client) nvr = client.bootstrap.nvr nvr._api = client diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py new file mode 100644 index 00000000000..e64a0a87377 --- /dev/null +++ b/tests/components/unifiprotect/test_views.py @@ -0,0 +1,427 @@ +"""Test UniFi Protect views.""" + +from datetime import datetime, timedelta +from typing import Any, cast +from unittest.mock import AsyncMock, Mock + +from aiohttp import ClientResponse +import pytest +from pyunifiprotect.data import Camera, Event, EventType +from pyunifiprotect.exceptions import ClientError + +from homeassistant.components.unifiprotect.views import ( + async_generate_event_video_url, + async_generate_thumbnail_url, +) +from homeassistant.core import HomeAssistant + +from .utils import MockUFPFixture, init_entry + +from tests.test_util.aiohttp import mock_aiohttp_client + + +async def test_thumbnail_bad_nvr_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid NVR ID in URL.""" + + ufp.api.get_event_thumbnail = AsyncMock() + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_event_thumbnail.assert_not_called + + +@pytest.mark.parametrize("width,height", [("test", None), (None, "test")]) +async def test_thumbnail_bad_params( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + width: Any, + height: Any, +) -> None: + """Test invalid bad query parameters.""" + + ufp.api.get_event_thumbnail = AsyncMock() + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url( + "test_id", ufp.api.bootstrap.nvr.id, width=width, height=height + ) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 400 + ufp.api.get_event_thumbnail.assert_not_called + + +async def test_thumbnail_bad_event( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid with error raised.""" + + ufp.api.get_event_thumbnail = AsyncMock(side_effect=ClientError()) + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + +async def test_thumbnail_no_data( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid no thumbnail returned.""" + + ufp.api.get_event_thumbnail = AsyncMock(return_value=None) + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + +async def test_thumbnail( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid NVR ID in URL.""" + + ufp.api.get_event_thumbnail = AsyncMock(return_value=b"testtest") + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 200 + assert response.content_type == "image/jpeg" + assert await response.content.read() == b"testtest" + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + +async def test_video_bad_event( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test generating event with bad camera ID.""" + + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id="test_id", + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + with pytest.raises(ValueError): + async_generate_event_video_url(event) + + +async def test_video_bad_event_ongoing( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test generating event with bad camera ID.""" + + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=None, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + with pytest.raises(ValueError): + async_generate_event_video_url(event) + + +async def test_video_bad_perms( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test generating event with bad user permissions.""" + + ufp.api.bootstrap.auth_user.all_permissions = [] + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + with pytest.raises(PermissionError): + async_generate_event_video_url(event) + + +async def test_video_bad_nvr_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with bad NVR id.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + url = url.replace(ufp.api.bootstrap.nvr.id, "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_not_called + + +async def test_video_bad_camera_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with bad camera id.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + url = url.replace(camera.id, "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_not_called + + +async def test_video_bad_camera_perms( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with bad camera perms.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + + ufp.api.bootstrap.auth_user.all_permissions = [] + ufp.api.bootstrap.auth_user._perm_cache = {} + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 403 + ufp.api.request.assert_not_called + + +@pytest.mark.parametrize("start,end", [("test", None), (None, "test")]) +async def test_video_bad_params( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, + start: Any, + end: Any, +) -> None: + """Test video URL with bad start/end params.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + from_value = event_start if start is not None else fixed_now + to_value = start if start is not None else end + url = url.replace(from_value.isoformat(), to_value) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 400 + ufp.api.request.assert_not_called + + +async def test_video_bad_video( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with no video.""" + + ufp.api.request = AsyncMock(side_effect=ClientError) + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_called_once + + +async def test_video( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with no video.""" + + content = Mock() + content.__anext__ = AsyncMock(side_effect=[b"test", b"test", StopAsyncIteration()]) + content.__aiter__ = Mock(return_value=content) + + mock_response = Mock() + mock_response.content_length = 8 + mock_response.content.iter_chunked = Mock(return_value=content) + + ufp.api.request = AsyncMock(return_value=mock_response) + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + assert await response.content.read() == b"testtest" + + assert response.status == 200 + ufp.api.request.assert_called_once From e2fe1a1c5dcfc385b000d5a7edbd4a03e6ec18cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Jun 2022 19:14:56 -0500 Subject: [PATCH 006/820] Allow tuple subclasses to be json serialized (#74207) --- homeassistant/helpers/json.py | 2 +- tests/helpers/test_json.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 8b91f5eb2b5..74a2f542910 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -33,7 +33,7 @@ def json_encoder_default(obj: Any) -> Any: Hand other objects to the original method. """ - if isinstance(obj, set): + if isinstance(obj, (set, tuple)): return list(obj) if isinstance(obj, float): return float(obj) diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index cfb403ca4a9..54c488690fa 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -1,6 +1,7 @@ """Test Home Assistant remote methods and classes.""" import datetime import json +import time import pytest @@ -87,3 +88,11 @@ def test_json_dumps_float_subclass(): """A float subclass.""" assert json_dumps({"c": FloatSubclass(1.2)}) == '{"c":1.2}' + + +def test_json_dumps_tuple_subclass(): + """Test the json dumps a tuple subclass.""" + + tt = time.struct_time((1999, 3, 17, 32, 44, 55, 2, 76, 0)) + + assert json_dumps(tt) == "[1999,3,17,32,44,55,2,76,0]" From 721741281ec1f71f97771995033568b5505c0eb4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 30 Jun 2022 00:23:07 +0000 Subject: [PATCH 007/820] [ci skip] Translation update --- .../alarmdecoder/translations/bg.json | 3 + .../components/androidtv/translations/bg.json | 9 ++ .../components/asuswrt/translations/bg.json | 1 + .../components/awair/translations/el.json | 7 ++ .../components/awair/translations/ja.json | 7 ++ .../awair/translations/zh-Hant.json | 7 ++ .../components/dexcom/translations/bg.json | 1 + .../enphase_envoy/translations/bg.json | 7 ++ .../components/ezviz/translations/bg.json | 8 ++ .../faa_delays/translations/bg.json | 1 + .../components/fritz/translations/bg.json | 3 + .../components/ialarm/translations/bg.json | 1 + .../keenetic_ndms2/translations/bg.json | 3 + .../components/kmtronic/translations/bg.json | 1 + .../components/lcn/translations/ca.json | 1 + .../components/lcn/translations/el.json | 1 + .../components/lcn/translations/ja.json | 1 + .../components/lcn/translations/no.json | 1 + .../components/lcn/translations/zh-Hant.json | 1 + .../components/life360/translations/ca.json | 25 +++++- .../components/life360/translations/de.json | 23 +++++ .../components/life360/translations/en.json | 84 ++++++++++--------- .../components/life360/translations/fr.json | 25 +++++- .../life360/translations/pt-BR.json | 25 +++++- .../life360/translations/zh-Hant.json | 25 +++++- .../components/lyric/translations/bg.json | 7 ++ .../components/monoprice/translations/bg.json | 3 + .../components/mqtt/translations/bg.json | 5 ++ .../components/mysensors/translations/bg.json | 1 + .../components/nina/translations/ca.json | 22 +++++ .../components/nina/translations/de.json | 22 +++++ .../components/nina/translations/el.json | 22 +++++ .../components/nina/translations/fr.json | 40 +++++++-- .../components/nina/translations/ja.json | 21 +++++ .../components/nina/translations/pt-BR.json | 22 +++++ .../components/nina/translations/zh-Hant.json | 22 +++++ .../components/picnic/translations/bg.json | 2 + .../components/roku/translations/bg.json | 3 + .../components/sense/translations/bg.json | 1 + .../somfy_mylink/translations/bg.json | 1 + .../components/subaru/translations/bg.json | 1 + .../components/verisure/translations/bg.json | 3 +- .../wolflink/translations/sensor.bg.json | 1 + .../zoneminder/translations/bg.json | 3 +- 44 files changed, 419 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/lyric/translations/bg.json diff --git a/homeassistant/components/alarmdecoder/translations/bg.json b/homeassistant/components/alarmdecoder/translations/bg.json index b918c0c7710..bf4f4b7175c 100644 --- a/homeassistant/components/alarmdecoder/translations/bg.json +++ b/homeassistant/components/alarmdecoder/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "protocol": { "data": { diff --git a/homeassistant/components/androidtv/translations/bg.json b/homeassistant/components/androidtv/translations/bg.json index f912f17d257..5407acc2f55 100644 --- a/homeassistant/components/androidtv/translations/bg.json +++ b/homeassistant/components/androidtv/translations/bg.json @@ -16,5 +16,14 @@ } } } + }, + "options": { + "step": { + "apps": { + "data": { + "app_name": "\u0418\u043c\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/bg.json b/homeassistant/components/asuswrt/translations/bg.json index df452e48980..a407de4c584 100644 --- a/homeassistant/components/asuswrt/translations/bg.json +++ b/homeassistant/components/asuswrt/translations/bg.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 0acefe23c02..e878c370d93 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -17,6 +17,13 @@ }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair." }, + "reauth_confirm": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "email": "Email" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair." + }, "user": { "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 83121c9fe42..7c7b73b312f 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -17,6 +17,13 @@ }, "description": "Awair developer access token\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "reauth_confirm": { + "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "email": "E\u30e1\u30fc\u30eb" + }, + "description": "Awair developer access token\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index 0bd7749c65f..f14acef8550 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -17,6 +17,13 @@ }, "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" }, + "reauth_confirm": { + "data": { + "access_token": "\u5b58\u53d6\u6b0a\u6756", + "email": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" + }, "user": { "data": { "access_token": "\u5b58\u53d6\u6b0a\u6756", diff --git a/homeassistant/components/dexcom/translations/bg.json b/homeassistant/components/dexcom/translations/bg.json index ec574a06c2f..f0b893b6182 100644 --- a/homeassistant/components/dexcom/translations/bg.json +++ b/homeassistant/components/dexcom/translations/bg.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/enphase_envoy/translations/bg.json b/homeassistant/components/enphase_envoy/translations/bg.json index ffb593eb287..1fce5cf396e 100644 --- a/homeassistant/components/enphase_envoy/translations/bg.json +++ b/homeassistant/components/enphase_envoy/translations/bg.json @@ -5,6 +5,13 @@ }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/bg.json b/homeassistant/components/ezviz/translations/bg.json index 702e3b80001..7e54efd88a9 100644 --- a/homeassistant/components/ezviz/translations/bg.json +++ b/homeassistant/components/ezviz/translations/bg.json @@ -1,7 +1,15 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{serial}", "step": { + "confirm": { + "data": { + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "user": { "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Ezviz Cloud" } diff --git a/homeassistant/components/faa_delays/translations/bg.json b/homeassistant/components/faa_delays/translations/bg.json index 93fa3f04d6c..ac15641f743 100644 --- a/homeassistant/components/faa_delays/translations/bg.json +++ b/homeassistant/components/faa_delays/translations/bg.json @@ -4,6 +4,7 @@ "already_configured": "\u0422\u043e\u0432\u0430 \u043b\u0435\u0442\u0438\u0449\u0435 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e." }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_airport": "\u041a\u043e\u0434\u044a\u0442 \u043d\u0430 \u043b\u0435\u0442\u0438\u0449\u0435\u0442\u043e \u043d\u0435 \u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index b699cec829c..fa080a7662e 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/ialarm/translations/bg.json b/homeassistant/components/ialarm/translations/bg.json index 4983c9a14b2..09f0ff26e5d 100644 --- a/homeassistant/components/ialarm/translations/bg.json +++ b/homeassistant/components/ialarm/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/keenetic_ndms2/translations/bg.json b/homeassistant/components/keenetic_ndms2/translations/bg.json index 42c3174a4c4..4105afcbe4d 100644 --- a/homeassistant/components/keenetic_ndms2/translations/bg.json +++ b/homeassistant/components/keenetic_ndms2/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/kmtronic/translations/bg.json b/homeassistant/components/kmtronic/translations/bg.json index d152ddfcf20..737855f7b76 100644 --- a/homeassistant/components/kmtronic/translations/bg.json +++ b/homeassistant/components/kmtronic/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/lcn/translations/ca.json b/homeassistant/components/lcn/translations/ca.json index e1c08f18137..940e212a9f0 100644 --- a/homeassistant/components/lcn/translations/ca.json +++ b/homeassistant/components/lcn/translations/ca.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "codi de bloqueig rebut", "fingerprint": "codi d'empremta rebut", "send_keys": "claus d'enviament rebudes", "transmitter": "codi del transmissor rebut", diff --git a/homeassistant/components/lcn/translations/el.json b/homeassistant/components/lcn/translations/el.json index ae71f96d361..e0086482539 100644 --- a/homeassistant/components/lcn/translations/el.json +++ b/homeassistant/components/lcn/translations/el.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u03b5\u03bb\u03ae\u03c6\u03b8\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd", "fingerprint": "\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03b1\u03ba\u03c4\u03c5\u03bb\u03b9\u03ba\u03bf\u03cd \u03b1\u03c0\u03bf\u03c4\u03c5\u03c0\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", "send_keys": "\u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03ce\u03bd \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", "transmitter": "\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03bf\u03bc\u03c0\u03bf\u03cd \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", diff --git a/homeassistant/components/lcn/translations/ja.json b/homeassistant/components/lcn/translations/ja.json index b656835dcbc..30849a56dc5 100644 --- a/homeassistant/components/lcn/translations/ja.json +++ b/homeassistant/components/lcn/translations/ja.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u30b3\u30fc\u30c9\u30ed\u30c3\u30af\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f", "fingerprint": "\u6307\u7d0b\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(fingerprint code received)", "send_keys": "\u9001\u4fe1\u30ad\u30fc\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(send keys received)", "transmitter": "\u9001\u4fe1\u6a5f\u30b3\u30fc\u30c9\u53d7\u4fe1\u3057\u307e\u3057\u305f(transmitter code received)", diff --git a/homeassistant/components/lcn/translations/no.json b/homeassistant/components/lcn/translations/no.json index 7ae5b70fe2f..cf780a06ba8 100644 --- a/homeassistant/components/lcn/translations/no.json +++ b/homeassistant/components/lcn/translations/no.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "kodel\u00e5skode mottatt", "fingerprint": "fingeravtrykkkode mottatt", "send_keys": "sende n\u00f8kler mottatt", "transmitter": "senderkode mottatt", diff --git a/homeassistant/components/lcn/translations/zh-Hant.json b/homeassistant/components/lcn/translations/zh-Hant.json index fe80da6694f..e6b104596b0 100644 --- a/homeassistant/components/lcn/translations/zh-Hant.json +++ b/homeassistant/components/lcn/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u5df2\u6536\u5230\u9396\u5b9a\u78bc", "fingerprint": "\u5df2\u6536\u5230\u6307\u7d0b\u78bc", "send_keys": "\u5df2\u6536\u5230\u50b3\u9001\u91d1\u9470", "transmitter": "\u5df2\u6536\u5230\u767c\u5c04\u5668\u78bc", diff --git a/homeassistant/components/life360/translations/ca.json b/homeassistant/components/life360/translations/ca.json index 875692a661a..f6b7a081863 100644 --- a/homeassistant/components/life360/translations/ca.json +++ b/homeassistant/components/life360/translations/ca.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "El compte ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_username": "Nom d'usuari incorrecte", "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", "username": "Nom d'usuari" }, "description": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url}). Pot ser que ho hagis de fer abans d'afegir cap compte.", - "title": "Informaci\u00f3 del compte Life360" + "title": "Configuraci\u00f3 del compte Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostra la conducci\u00f3 com a estat", + "driving_speed": "Velocitat de conducci\u00f3", + "limit_gps_acc": "Limita la precisi\u00f3 del GPS", + "max_gps_accuracy": "Precisi\u00f3 m\u00e0xima del GPS (metres)", + "set_drive_speed": "Configura el llindar de velocitat de conducci\u00f3" + }, + "title": "Opcions del compte" } } } diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 516b0255349..67f014e0a2c 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_username": "Ung\u00fcltiger Benutzername", "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", @@ -23,5 +32,19 @@ "title": "Life360-Kontoinformationen" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Fahren als Zustand anzeigen", + "driving_speed": "Fahrgeschwindigkeit", + "limit_gps_acc": "GPS-Genauigkeit einschr\u00e4nken", + "max_gps_accuracy": "Maximale GPS-Genauigkeit (Meter)", + "set_drive_speed": "Schwellenwert f\u00fcr die Fahrgeschwindigkeit festlegen" + }, + "title": "Kontoeinstellungen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/en.json b/homeassistant/components/life360/translations/en.json index b4c9eb452f6..4e7ff35c814 100644 --- a/homeassistant/components/life360/translations/en.json +++ b/homeassistant/components/life360/translations/en.json @@ -1,44 +1,50 @@ { - "config": { - "step": { - "user": { - "title": "Configure Life360 Account", - "data": { - "username": "Username", - "password": "Password" + "config": { + "abort": { + "already_configured": "Account is already configured", + "invalid_auth": "Invalid authentication", + "reauth_successful": "Re-authentication was successful", + "unknown": "Unexpected error" + }, + "create_entry": { + "default": "To set advanced options, see [Life360 documentation]({docs_url})." + }, + "error": { + "already_configured": "Account is already configured", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_username": "Invalid username", + "unknown": "Unexpected error" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "To set advanced options, see [Life360 documentation]({docs_url}).\nYou may want to do that before adding accounts.", + "title": "Configure Life360 Account" + } } - }, - "reauth_confirm": { - "title": "Reauthenticate Integration", - "data": { - "password": "Password" - } - } }, - "error": { - "invalid_auth": "Invalid authentication", - "already_configured": "Account is already configured", - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" - }, - "abort": { - "invalid_auth": "Invalid authentication", - "already_configured": "Account is already configured", - "reauth_successful": "Re-authentication was successful" - } - }, - "options": { - "step": { - "init": { - "title": "Account Options", - "data": { - "limit_gps_acc": "Limit GPS accuracy", - "max_gps_accuracy": "Max GPS accuracy (meters)", - "set_drive_speed": "Set driving speed threshold", - "driving_speed": "Driving speed", - "driving": "Show driving as state" + "options": { + "step": { + "init": { + "data": { + "driving": "Show driving as state", + "driving_speed": "Driving speed", + "limit_gps_acc": "Limit GPS accuracy", + "max_gps_accuracy": "Max GPS accuracy (meters)", + "set_drive_speed": "Set driving speed threshold" + }, + "title": "Account Options" + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json index f58b789b267..ce1fd3f7757 100644 --- a/homeassistant/components/life360/translations/fr.json +++ b/homeassistant/components/life360/translations/fr.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "invalid_auth": "Authentification non valide", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "unknown": "Erreur inattendue" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification non valide", "invalid_username": "Nom d'utilisateur non valide", "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" }, "description": "Pour d\u00e9finir des options avanc\u00e9es, voir [Documentation Life360]({docs_url}).\nVous pouvez le faire avant d'ajouter des comptes.", - "title": "Informations sur le compte Life360" + "title": "Configuration du compte Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Afficher la conduite comme \u00e9tat", + "driving_speed": "Vitesse de conduite", + "limit_gps_acc": "Limiter la pr\u00e9cision du GPS", + "max_gps_accuracy": "Pr\u00e9cision maximale du GPS (en m\u00e8tres)", + "set_drive_speed": "D\u00e9finition du seuil de vitesse de conduite" + }, + "title": "Options de compte" } } } diff --git a/homeassistant/components/life360/translations/pt-BR.json b/homeassistant/components/life360/translations/pt-BR.json index 7753c0f84dc..13349bcef67 100644 --- a/homeassistant/components/life360/translations/pt-BR.json +++ b/homeassistant/components/life360/translations/pt-BR.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "Integra\u00e7\u00e3o Reautenticar", "unknown": "Erro inesperado" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "A conta j\u00e1 foi configurada", + "cannot_connect": "Falhou ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Senha", "username": "Usu\u00e1rio" }, "description": "Para definir op\u00e7\u00f5es avan\u00e7adas, consulte [Documenta\u00e7\u00e3o da Life360] ({docs_url}). \n Voc\u00ea pode querer fazer isso antes de adicionar contas.", - "title": "Informa\u00e7\u00f5es da conta Life360" + "title": "Configurar conta Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostrar condu\u00e7\u00e3o como estado", + "driving_speed": "Velocidade de condu\u00e7\u00e3o", + "limit_gps_acc": "Limitar a precis\u00e3o do GPS", + "max_gps_accuracy": "Precis\u00e3o m\u00e1xima do GPS (metros)", + "set_drive_speed": "Definir limite de velocidade de condu\u00e7\u00e3o" + }, + "title": "Op\u00e7\u00f5es da conta" } } } diff --git a/homeassistant/components/life360/translations/zh-Hant.json b/homeassistant/components/life360/translations/zh-Hant.json index ad7fde2e21d..55e55bb30c7 100644 --- a/homeassistant/components/life360/translations/zh-Hant.json +++ b/homeassistant/components/life360/translations/zh-Hant.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_username": "\u4f7f\u7528\u8005\u540d\u7a31\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u6b32\u8a2d\u5b9a\u9032\u968e\u9078\u9805\uff0c\u8acb\u53c3\u95b1 [Life360 \u6587\u4ef6]({docs_url})\u3002\n\u5efa\u8b70\u65bc\u65b0\u589e\u5e33\u865f\u524d\uff0c\u5148\u9032\u884c\u4e86\u89e3\u3002", - "title": "Life360 \u5e33\u865f\u8cc7\u8a0a" + "title": "\u8a2d\u5b9a Life360 \u5e33\u865f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u5c07\u884c\u99db\u4e2d\u986f\u793a\u70ba\u72c0\u614b", + "driving_speed": "\u884c\u99db\u901f\u5ea6", + "limit_gps_acc": "\u9650\u5236 GPS \u7cbe\u51c6\u5ea6", + "max_gps_accuracy": "\u6700\u9ad8 GPS \u7cbe\u78ba\u5ea6\uff08\u516c\u5c3a\uff09", + "set_drive_speed": "\u8a2d\u5b9a\u901f\u9650\u503c" + }, + "title": "\u5e33\u865f\u9078\u9805" } } } diff --git a/homeassistant/components/lyric/translations/bg.json b/homeassistant/components/lyric/translations/bg.json new file mode 100644 index 00000000000..2f756377e31 --- /dev/null +++ b/homeassistant/components/lyric/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/bg.json b/homeassistant/components/monoprice/translations/bg.json index 40cf11b9651..0dbc6308db2 100644 --- a/homeassistant/components/monoprice/translations/bg.json +++ b/homeassistant/components/monoprice/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index f69eb1b4cc9..65260eacabb 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -27,6 +27,11 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" + } + }, "options": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index 7c8e0080bc2..8abf7bbd1fa 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -8,6 +8,7 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_ip": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441", "invalid_port": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442", diff --git a/homeassistant/components/nina/translations/ca.json b/homeassistant/components/nina/translations/ca.json index 41d274d779d..1ff79090d3c 100644 --- a/homeassistant/components/nina/translations/ca.json +++ b/homeassistant/components/nina/translations/ca.json @@ -23,5 +23,27 @@ "title": "Selecciona ciutat/comtat" } } + }, + "options": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_selection": "Seleccioneu almenys una ciutat/comtat", + "unknown": "Error inesperat" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Ciutat/comtat (A-D)", + "_e_to_h": "Ciutat/comtat (E-H)", + "_i_to_l": "Ciutat/comtat (I-L)", + "_m_to_q": "Ciutat/comtat (M-Q)", + "_r_to_u": "Ciutat/comtat (R-U)", + "_v_to_z": "Ciutat/comtat (V-Z)", + "corona_filter": "Elimina els avisos de corona", + "slots": "Nombre d'avisos m\u00e0xims per ciutat/comtat" + }, + "title": "Opcions" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/de.json b/homeassistant/components/nina/translations/de.json index 1e7e1a3e70e..4e6b881b051 100644 --- a/homeassistant/components/nina/translations/de.json +++ b/homeassistant/components/nina/translations/de.json @@ -23,5 +23,27 @@ "title": "Stadt/Landkreis ausw\u00e4hlen" } } + }, + "options": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "no_selection": "Bitte w\u00e4hle mindestens eine Stadt/einen Landkreis aus", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Stadt/Landkreis (A-D)", + "_e_to_h": "Stadt/Landkreis (E-H)", + "_i_to_l": "Stadt/Landkreis (I-L)", + "_m_to_q": "Stadt/Landkreis (M-Q)", + "_r_to_u": "Stadt/Landkreis (R-U)", + "_v_to_z": "Stadt/Landkreis (V-Z)", + "corona_filter": "Corona-Warnungen entfernen", + "slots": "Maximale Warnungen pro Stadt/Landkreis" + }, + "title": "Optionen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/el.json b/homeassistant/components/nina/translations/el.json index 9faf567662a..8466a9e5215 100644 --- a/homeassistant/components/nina/translations/el.json +++ b/homeassistant/components/nina/translations/el.json @@ -23,5 +23,27 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03cc\u03bb\u03b7\u03c2/\u03bd\u03bf\u03bc\u03bf\u03cd" } } + }, + "options": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_selection": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03af\u03b1 \u03c0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (\u0391-D)", + "_e_to_h": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (\u0395-\u0397)", + "_i_to_l": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (I-L)", + "_m_to_q": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (\u039c-Q)", + "_r_to_u": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (R-U)", + "_v_to_z": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (V-Z)", + "corona_filter": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd Corona", + "slots": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b5\u03c2 \u03c0\u03c1\u03bf\u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b1\u03bd\u03ac \u03c0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/fr.json b/homeassistant/components/nina/translations/fr.json index 83f994b79a4..7d76b2aa5db 100644 --- a/homeassistant/components/nina/translations/fr.json +++ b/homeassistant/components/nina/translations/fr.json @@ -5,22 +5,44 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "no_selection": "Veuillez s\u00e9lectionner au moins une ville/une region", + "no_selection": "Veuillez s\u00e9lectionner au moins une ville ou un comt\u00e9", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { - "_a_to_d": "Ville/R\u00e9gion (A-D)", - "_e_to_h": "Ville/R\u00e9gion (E-H)", - "_i_to_l": "Ville/R\u00e9gion (I-L)", - "_m_to_q": "Ville/R\u00e9gion (M-Q)", - "_r_to_u": "Ville/R\u00e9gion (R-U)", - "_v_to_z": "Ville/R\u00e9gion (V-Z)", + "_a_to_d": "Ville / comt\u00e9 (A-D)", + "_e_to_h": "Ville / comt\u00e9 (E-H)", + "_i_to_l": "Ville / comt\u00e9 (I-L)", + "_m_to_q": "Ville / comt\u00e9 (M-Q)", + "_r_to_u": "Ville / comt\u00e9 (R-U)", + "_v_to_z": "Ville / comt\u00e9 (V-Z)", "corona_filter": "Supprimer les avertissements Corona", - "slots": "Nombre maximal d'avertissements par ville/region" + "slots": "Nombre maximal d'avertissements par ville ou comt\u00e9" }, - "title": "S\u00e9lectionnez la ville/la region" + "title": "S\u00e9lectionnez la ville ou le comt\u00e9" + } + } + }, + "options": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "no_selection": "Veuillez s\u00e9lectionner au moins une ville ou un comt\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Ville / comt\u00e9 (A-D)", + "_e_to_h": "Ville / comt\u00e9 (E-H)", + "_i_to_l": "Ville / comt\u00e9 (I-L)", + "_m_to_q": "Ville / comt\u00e9 (M-Q)", + "_r_to_u": "Ville / comt\u00e9 (R-U)", + "_v_to_z": "Ville / comt\u00e9 (V-Z)", + "corona_filter": "Supprimer les avertissements Corona", + "slots": "Nombre maximal d'avertissements par ville ou comt\u00e9" + }, + "title": "Options" } } } diff --git a/homeassistant/components/nina/translations/ja.json b/homeassistant/components/nina/translations/ja.json index 7c765025ae8..5e3978f4777 100644 --- a/homeassistant/components/nina/translations/ja.json +++ b/homeassistant/components/nina/translations/ja.json @@ -23,5 +23,26 @@ "title": "\u5e02\u533a\u753a\u6751/\u7fa4\u3092\u9078\u629e" } } + }, + "options": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "no_selection": "\u5c11\u306a\u304f\u3068\u30821\u3064\u306e\u5e02\u533a\u753a\u6751/\u90e1\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "init": { + "data": { + "_a_to_d": "City/county (A-D)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "City/county (M-Q)", + "_r_to_u": "City/county (R-U)", + "_v_to_z": "City/county (V-Z)", + "corona_filter": "\u30b3\u30ed\u30ca\u8b66\u544a\u306e\u524a\u9664", + "slots": "1\u5e02\u533a\u753a\u6751/\u90e1\u3042\u305f\u308a\u306e\u6700\u5927\u8b66\u544a\u6570" + }, + "title": "\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pt-BR.json b/homeassistant/components/nina/translations/pt-BR.json index c22d3e06530..3da9c79f05f 100644 --- a/homeassistant/components/nina/translations/pt-BR.json +++ b/homeassistant/components/nina/translations/pt-BR.json @@ -23,5 +23,27 @@ "title": "Selecione a cidade/munic\u00edpio" } } + }, + "options": { + "error": { + "cannot_connect": "Falhou ao conectar", + "no_selection": "Selecione pelo menos uma cidade/munic\u00edpio", + "unknown": "Erro inesperado" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Cidade/munic\u00edpio (A-D)", + "_e_to_h": "Cidade/munic\u00edpio (E-H)", + "_i_to_l": "Cidade/munic\u00edpio (I-L)", + "_m_to_q": "Cidade/munic\u00edpio (M-Q)", + "_r_to_u": "Cidade/munic\u00edpio (R-U)", + "_v_to_z": "Cidade/munic\u00edpio (V-Z)", + "corona_filter": "Remover avisos de corona", + "slots": "M\u00e1ximo de avisos por cidade/munic\u00edpio" + }, + "title": "Op\u00e7\u00f5es" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/zh-Hant.json b/homeassistant/components/nina/translations/zh-Hant.json index 212c65070d8..dde251012a0 100644 --- a/homeassistant/components/nina/translations/zh-Hant.json +++ b/homeassistant/components/nina/translations/zh-Hant.json @@ -23,5 +23,27 @@ "title": "\u9078\u64c7\u57ce\u5e02/\u7e23\u5e02" } } + }, + "options": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_selection": "\u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u500b\u57ce\u5e02/\u7e23\u5e02", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u57ce\u5e02/\u7e23\u5e02\uff08A-D\uff09", + "_e_to_h": "\u57ce\u5e02/\u7e23\u5e02\uff08E-H\uff09", + "_i_to_l": "\u57ce\u5e02/\u7e23\u5e02\uff08I-L\uff09", + "_m_to_q": "\u57ce\u5e02/\u7e23\u5e02\uff08M-Q\uff09", + "_r_to_u": "\u57ce\u5e02/\u7e23\u5e02\uff08R-U\uff09", + "_v_to_z": "\u57ce\u5e02/\u7e23\u5e02\uff08V-Z\uff09", + "corona_filter": "\u79fb\u9664 Corona \u8b66\u544a", + "slots": "\u6bcf\u500b\u57ce\u5e02/\u7e23\u5e02\u6700\u5927\u8b66\u544a\u503c" + }, + "title": "\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/bg.json b/homeassistant/components/picnic/translations/bg.json index 32ea4287182..aaf9f767fff 100644 --- a/homeassistant/components/picnic/translations/bg.json +++ b/homeassistant/components/picnic/translations/bg.json @@ -1,9 +1,11 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/roku/translations/bg.json b/homeassistant/components/roku/translations/bg.json index ef4bfded40c..efb4c18e1b0 100644 --- a/homeassistant/components/roku/translations/bg.json +++ b/homeassistant/components/roku/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "discovery_confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" diff --git a/homeassistant/components/sense/translations/bg.json b/homeassistant/components/sense/translations/bg.json index 2be0802eef9..f81ad124c51 100644 --- a/homeassistant/components/sense/translations/bg.json +++ b/homeassistant/components/sense/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { diff --git a/homeassistant/components/somfy_mylink/translations/bg.json b/homeassistant/components/somfy_mylink/translations/bg.json index ca0ed419f99..5ee98a5d46c 100644 --- a/homeassistant/components/somfy_mylink/translations/bg.json +++ b/homeassistant/components/somfy_mylink/translations/bg.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index 9031d8b47ce..212303991b0 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { diff --git a/homeassistant/components/verisure/translations/bg.json b/homeassistant/components/verisure/translations/bg.json index 0f10e122185..cf602238bf3 100644 --- a/homeassistant/components/verisure/translations/bg.json +++ b/homeassistant/components/verisure/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/wolflink/translations/sensor.bg.json b/homeassistant/components/wolflink/translations/sensor.bg.json index 8d0335dcc31..6d41058320d 100644 --- a/homeassistant/components/wolflink/translations/sensor.bg.json +++ b/homeassistant/components/wolflink/translations/sensor.bg.json @@ -5,6 +5,7 @@ "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", "dhw_prior": "DHWPrior", "gasdruck": "\u041d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 \u0433\u0430\u0437\u0430", + "heizung": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "kalibration": "\u041a\u0430\u043b\u0438\u0431\u0440\u0438\u0440\u0430\u043d\u0435", "test": "\u0422\u0435\u0441\u0442", "tpw": "TPW", diff --git a/homeassistant/components/zoneminder/translations/bg.json b/homeassistant/components/zoneminder/translations/bg.json index a19fe59b023..ad4605eceb9 100644 --- a/homeassistant/components/zoneminder/translations/bg.json +++ b/homeassistant/components/zoneminder/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "auth_fail": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0430 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0438.", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "error": { "auth_fail": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0430 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0438.", From ab6e92f99696a8f5fab587dd56d07cc508c5b898 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 02:35:49 +0200 Subject: [PATCH 008/820] Patch out life360 entry setup in tests (#74212) --- tests/components/life360/test_config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py index 0b5b850ac23..c24852fddf7 100644 --- a/tests/components/life360/test_config_flow.py +++ b/tests/components/life360/test_config_flow.py @@ -118,7 +118,7 @@ async def test_user_show_form(hass, life360_api): assert keys[keys.index(key)].default == vol.UNDEFINED -async def test_user_config_flow_success(hass, life360_api): +async def test_user_config_flow_success(hass, life360_api, life360): """Test a successful user config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -228,7 +228,7 @@ async def test_reauth_config_flow_success(hass, life360_api, caplog, state): assert config_entry.data == TEST_CONFIG_DATA_2 -async def test_reauth_config_flow_login_error(hass, life360_api, caplog): +async def test_reauth_config_flow_login_error(hass, life360_api, life360, caplog): """Test a reauthorization config flow with a login error.""" config_entry = create_config_entry(hass) @@ -285,7 +285,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, caplog): # ========== Option flow Tests ========================================================= -async def test_options_flow(hass): +async def test_options_flow(hass, life360): """Test an options flow.""" config_entry = create_config_entry(hass) From 42533ebbb33369f666bcd9bec34b60d58e9898f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 03:40:58 +0200 Subject: [PATCH 009/820] Update requests to 2.28.1 (#74210) --- homeassistant/package_constraints.txt | 6 +----- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_test.txt | 2 +- script/gen_requirements_all.py | 4 ---- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 11ffd9c4c0c..9ba945c3e2b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ pyserial==3.5 python-slugify==4.0.1 pyudev==0.22.0 pyyaml==6.0 -requests==2.28.0 +requests==2.28.1 scapy==2.4.5 sqlalchemy==1.4.38 typing-extensions>=3.10.0.2,<5.0 @@ -114,7 +114,3 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 - -# Pin charset-normalizer to 2.0.12 due to version conflict. -# https://github.com/home-assistant/core/pull/74104 -charset-normalizer==2.0.12 diff --git a/pyproject.toml b/pyproject.toml index 5e5974d142f..f50c835e58f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", - "requests==2.28.0", + "requests==2.28.1", "typing-extensions>=3.10.0.2,<5.0", "voluptuous==0.13.1", "voluptuous-serialize==2.5.0", diff --git a/requirements.txt b/requirements.txt index 7506201eae1..98b148fa923 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ orjson==3.7.5 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 -requests==2.28.0 +requests==2.28.1 typing-extensions>=3.10.0.2,<5.0 voluptuous==0.13.1 voluptuous-serialize==2.5.0 diff --git a/requirements_test.txt b/requirements_test.txt index 046d8bfb400..6072ce896ee 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -42,6 +42,6 @@ types-pkg-resources==0.1.3 types-python-slugify==0.1.2 types-pytz==2021.1.2 types-PyYAML==5.4.6 -types-requests==2.27.30 +types-requests==2.28.0 types-toml==0.1.5 types-ujson==0.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c6b50c6bd32..a2a0eab897a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -132,10 +132,6 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 - -# Pin charset-normalizer to 2.0.12 due to version conflict. -# https://github.com/home-assistant/core/pull/74104 -charset-normalizer==2.0.12 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From f721b9e3df8d08d496fc96bf4a2ad24e78882b33 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 03:43:14 +0200 Subject: [PATCH 010/820] Fix clicksend request content type headers (#74189) --- homeassistant/components/clicksend/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 74f1c2e1ae5..ec6bed3c55d 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -3,7 +3,6 @@ from http import HTTPStatus import json import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -23,7 +22,7 @@ BASE_API_URL = "https://rest.clicksend.com/v3" DEFAULT_SENDER = "hass" TIMEOUT = 5 -HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} +HEADERS = {"Content-Type": CONTENT_TYPE_JSON} PLATFORM_SCHEMA = vol.Schema( From 555e9c676275c21f8e3d24cc52bd67136cc815d4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 07:06:35 +0200 Subject: [PATCH 011/820] Fix input_number invalid state restore handling (#74213) Co-authored-by: J. Nick Koston --- .../components/input_number/__init__.py | 7 ++++-- tests/components/input_number/test_init.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index a6ee8dd0f7d..8e922687e59 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -1,6 +1,7 @@ """Support to set a numeric value from a slider or text box.""" from __future__ import annotations +from contextlib import suppress import logging import voluptuous as vol @@ -281,8 +282,10 @@ class InputNumber(RestoreEntity): if self._current_value is not None: return - state = await self.async_get_last_state() - value = state and float(state.state) + value: float | None = None + if state := await self.async_get_last_state(): + with suppress(ValueError): + value = float(state.state) # Check against None because value can be 0 if value is not None and self._minimum <= value <= self._maximum: diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index ca496723d99..4149627720b 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -255,6 +255,29 @@ async def test_restore_state(hass): assert float(state.state) == 10 +async def test_restore_invalid_state(hass): + """Ensure an invalid restore state is handled.""" + mock_restore_cache( + hass, (State("input_number.b1", "="), State("input_number.b2", "200")) + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"b1": {"min": 2, "max": 100}, "b2": {"min": 10, "max": 100}}}, + ) + + state = hass.states.get("input_number.b1") + assert state + assert float(state.state) == 2 + + state = hass.states.get("input_number.b2") + assert state + assert float(state.state) == 10 + + async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( From bef512c4255e117680c5cf7024f79b217ce73b10 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 30 Jun 2022 07:09:52 +0200 Subject: [PATCH 012/820] Split attributes into sensors for here_travel_time (#72405) --- .../components/here_travel_time/__init__.py | 4 +- .../components/here_travel_time/const.py | 11 +- .../components/here_travel_time/sensor.py | 178 ++++++++++++------ .../here_travel_time/test_sensor.py | 114 ++++++----- 4 files changed, 187 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 2b9853c3b10..24da0bc2673 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -187,8 +187,8 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): return HERERoutingData( { ATTR_ATTRIBUTION: attribution, - ATTR_DURATION: summary["baseTime"] / 60, # type: ignore[misc] - ATTR_DURATION_IN_TRAFFIC: traffic_time / 60, + ATTR_DURATION: round(summary["baseTime"] / 60), # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: round(traffic_time / 60), ATTR_DISTANCE: distance, ATTR_ROUTE: response.route_short, ATTR_ORIGIN: ",".join(origin), diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index bde17f5c306..b3768b2d69d 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -24,8 +24,6 @@ CONF_DEPARTURE_TIME = "departure_time" DEFAULT_NAME = "HERE Travel Time" -TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] - TRAVEL_MODE_BICYCLE = "bicycle" TRAVEL_MODE_CAR = "car" TRAVEL_MODE_PEDESTRIAN = "pedestrian" @@ -41,7 +39,6 @@ TRAVEL_MODES = [ TRAVEL_MODE_TRUCK, ] -TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] TRAFFIC_MODE_ENABLED = "traffic_enabled" @@ -58,6 +55,14 @@ ICON_PEDESTRIAN = "mdi:walk" ICON_PUBLIC = "mdi:bus" ICON_TRUCK = "mdi:truck" +ICONS = { + TRAVEL_MODE_BICYCLE: ICON_BICYCLE, + TRAVEL_MODE_PEDESTRIAN: ICON_PEDESTRIAN, + TRAVEL_MODE_PUBLIC: ICON_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE: ICON_PUBLIC, + TRAVEL_MODE_TRUCK: ICON_TRUCK, +} + UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] ATTR_DURATION = "duration" diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 75c9fd2ea3b..c4be60d5569 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,16 +1,24 @@ """Support for HERE travel time sensors.""" from __future__ import annotations +from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_MODE, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_API_KEY, CONF_MODE, CONF_NAME, @@ -28,10 +36,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import HereTravelTimeDataUpdateCoordinator from .const import ( + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, ATTR_DURATION, ATTR_DURATION_IN_TRAFFIC, - ATTR_TRAFFIC_MODE, - ATTR_UNIT_SYSTEM, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + ATTR_ROUTE, CONF_ARRIVAL, CONF_DEPARTURE, CONF_DESTINATION_ENTITY_ID, @@ -44,14 +56,10 @@ from .const import ( CONF_TRAFFIC_MODE, DEFAULT_NAME, DOMAIN, - ICON_BICYCLE, ICON_CAR, - ICON_PEDESTRIAN, - ICON_PUBLIC, - ICON_TRUCK, + ICONS, ROUTE_MODE_FASTEST, ROUTE_MODES, - TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, @@ -59,7 +67,6 @@ from .const import ( TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, TRAVEL_MODES, - TRAVEL_MODES_PUBLIC, UNITS, ) @@ -115,6 +122,69 @@ PLATFORM_SCHEMA = vol.All( ) +def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]: + """Construct SensorEntityDescriptions.""" + return ( + SensorEntityDescription( + name="Duration", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + name="Duration in Traffic", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DURATION_IN_TRAFFIC, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + name="Distance", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + name="Route", + icon="mdi:directions", + key=ATTR_ROUTE, + ), + ) + + +def create_origin_sensor( + config_entry: ConfigEntry, hass: HomeAssistant +) -> OriginSensor: + """Create a origin sensor.""" + return OriginSensor( + config_entry.entry_id, + config_entry.data[CONF_NAME], + SensorEntityDescription( + name="Origin", + icon="mdi:store-marker", + key=ATTR_ORIGIN_NAME, + ), + hass.data[DOMAIN][config_entry.entry_id], + ) + + +def create_destination_sensor( + config_entry: ConfigEntry, hass: HomeAssistant +) -> DestinationSensor: + """Create a destination sensor.""" + return DestinationSensor( + config_entry.entry_id, + config_entry.data[CONF_NAME], + SensorEntityDescription( + name="Destination", + icon="mdi:store-marker", + key=ATTR_DESTINATION_NAME, + ), + hass.data[DOMAIN][config_entry.entry_id], + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -143,16 +213,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add HERE travel time entities from a config_entry.""" - async_add_entities( - [ + + sensors: list[HERETravelTimeSensor] = [] + for sensor_description in sensor_descriptions(config_entry.data[CONF_MODE]): + sensors.append( HERETravelTimeSensor( config_entry.entry_id, config_entry.data[CONF_NAME], - config_entry.options[CONF_TRAFFIC_MODE], + sensor_description, hass.data[DOMAIN][config_entry.entry_id], ) - ], - ) + ) + sensors.append(create_origin_sensor(config_entry, hass)) + sensors.append(create_destination_sensor(config_entry, hass)) + async_add_entities(sensors) class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): @@ -162,15 +236,14 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): self, unique_id_prefix: str, name: str, - traffic_mode: str, + sensor_description: SensorEntityDescription, coordinator: HereTravelTimeDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._traffic_mode = traffic_mode == TRAFFIC_MODE_ENABLED - self._attr_native_unit_of_measurement = TIME_MINUTES - self._attr_name = name - self._attr_unique_id = unique_id_prefix + self.entity_description = sensor_description + self._attr_name = f"{name} {sensor_description.name}" + self._attr_unique_id = f"{unique_id_prefix}_{sensor_description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id_prefix)}, entry_type=DeviceEntryType.SERVICE, @@ -188,34 +261,10 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): self.async_on_remove(async_at_start(self.hass, _update_at_start)) @property - def native_value(self) -> str | None: + def native_value(self) -> str | float | None: """Return the state of the sensor.""" if self.coordinator.data is not None: - return str( - round( - self.coordinator.data.get( - ATTR_DURATION_IN_TRAFFIC - if self._traffic_mode - else ATTR_DURATION - ) - ) - ) - return None - - @property - def extra_state_attributes( - self, - ) -> dict[str, None | float | str | bool] | None: - """Return the state attributes.""" - if self.coordinator.data is not None: - res = { - ATTR_UNIT_SYSTEM: self.coordinator.config.units, - ATTR_MODE: self.coordinator.config.travel_mode, - ATTR_TRAFFIC_MODE: self._traffic_mode, - **self.coordinator.data, - } - res.pop(ATTR_ATTRIBUTION) - return res + return self.coordinator.data.get(self.entity_description.key) return None @property @@ -225,15 +274,30 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): return self.coordinator.data.get(ATTR_ATTRIBUTION) return None + +class OriginSensor(HERETravelTimeSensor): + """Sensor holding information about the route origin.""" + @property - def icon(self) -> str: - """Icon to use in the frontend depending on travel_mode.""" - if self.coordinator.config.travel_mode == TRAVEL_MODE_BICYCLE: - return ICON_BICYCLE - if self.coordinator.config.travel_mode == TRAVEL_MODE_PEDESTRIAN: - return ICON_PEDESTRIAN - if self.coordinator.config.travel_mode in TRAVEL_MODES_PUBLIC: - return ICON_PUBLIC - if self.coordinator.config.travel_mode == TRAVEL_MODE_TRUCK: - return ICON_TRUCK - return ICON_CAR + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """GPS coordinates.""" + if self.coordinator.data is not None: + return { + ATTR_LATITUDE: self.coordinator.data[ATTR_ORIGIN].split(",")[0], + ATTR_LONGITUDE: self.coordinator.data[ATTR_ORIGIN].split(",")[1], + } + return None + + +class DestinationSensor(HERETravelTimeSensor): + """Sensor holding information about the route destination.""" + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """GPS coordinates.""" + if self.coordinator.data is not None: + return { + ATTR_LATITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[0], + ATTR_LONGITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[1], + } + return None diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index dc9ba128c35..93fbd1bd204 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -7,14 +7,6 @@ import pytest from homeassistant.components.here_travel_time.config_flow import default_options from homeassistant.components.here_travel_time.const import ( - ATTR_DESTINATION, - ATTR_DESTINATION_NAME, - ATTR_DISTANCE, - ATTR_DURATION, - ATTR_DURATION_IN_TRAFFIC, - ATTR_ORIGIN, - ATTR_ORIGIN_NAME, - ATTR_ROUTE, CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, CONF_DESTINATION_ENTITY_ID, @@ -24,7 +16,6 @@ from homeassistant.components.here_travel_time.const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, CONF_UNIT_SYSTEM, DOMAIN, ICON_BICYCLE, @@ -34,18 +25,18 @@ from homeassistant.components.here_travel_time.const import ( ICON_TRUCK, NO_ROUTE_ERROR_MESSAGE, ROUTE_MODE_FASTEST, - TRAFFIC_MODE_DISABLED, TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, - TRAVEL_MODES_VEHICLE, ) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_API_KEY, CONF_MODE, CONF_NAME, @@ -67,62 +58,57 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - "mode,icon,traffic_mode,unit_system,arrival_time,departure_time,expected_state,expected_distance,expected_duration_in_traffic", + "mode,icon,unit_system,arrival_time,departure_time,expected_duration,expected_distance,expected_duration_in_traffic", [ ( TRAVEL_MODE_CAR, ICON_CAR, - TRAFFIC_MODE_ENABLED, "metric", None, None, + "30", + "23.903", "31", - 23.903, - 31.016666666666666, ), ( TRAVEL_MODE_BICYCLE, ICON_BICYCLE, - TRAFFIC_MODE_DISABLED, "metric", None, None, "30", - 23.903, - 30.05, + "23.903", + "30", ), ( TRAVEL_MODE_PEDESTRIAN, ICON_PEDESTRIAN, - TRAFFIC_MODE_DISABLED, "imperial", None, None, "30", - 14.852631013, - 30.05, + "14.852631013", + "30", ), ( TRAVEL_MODE_PUBLIC_TIME_TABLE, ICON_PUBLIC, - TRAFFIC_MODE_DISABLED, "imperial", "08:00:00", None, "30", - 14.852631013, - 30.05, + "14.852631013", + "30", ), ( TRAVEL_MODE_TRUCK, ICON_TRUCK, - TRAFFIC_MODE_ENABLED, "metric", None, "08:00:00", + "30", + "23.903", "31", - 23.903, - 31.016666666666666, ), ], ) @@ -131,11 +117,10 @@ async def test_sensor( hass: HomeAssistant, mode, icon, - traffic_mode, unit_system, arrival_time, departure_time, - expected_state, + expected_duration, expected_distance, expected_duration_in_traffic, ): @@ -153,7 +138,6 @@ async def test_sensor( CONF_NAME: "test", }, options={ - CONF_TRAFFIC_MODE: traffic_mode, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, CONF_ARRIVAL_TIME: arrival_time, CONF_DEPARTURE_TIME: departure_time, @@ -166,44 +150,57 @@ async def test_sensor( hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES + duration = hass.states.get("sensor.test_duration") + assert duration.attributes.get("unit_of_measurement") == TIME_MINUTES assert ( - sensor.attributes.get(ATTR_ATTRIBUTION) + duration.attributes.get(ATTR_ATTRIBUTION) == "With the support of HERE Technologies. All information is provided without warranty of any kind." ) - assert sensor.state == expected_state + assert duration.attributes.get(ATTR_ICON) == icon + assert duration.state == expected_duration - assert sensor.attributes.get(ATTR_DURATION) == 30.05 - assert sensor.attributes.get(ATTR_DISTANCE) == expected_distance - assert sensor.attributes.get(ATTR_ROUTE) == ( + assert ( + hass.states.get("sensor.test_duration_in_traffic").state + == expected_duration_in_traffic + ) + assert hass.states.get("sensor.test_distance").state == expected_distance + assert hass.states.get("sensor.test_route").state == ( "US-29 - K St NW; US-29 - Whitehurst Fwy; " "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == unit_system assert ( - sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == expected_duration_in_traffic + hass.states.get("sensor.test_duration_in_traffic").state + == expected_duration_in_traffic ) - assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( - [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE] + assert hass.states.get("sensor.test_origin").state == "22nd St NW" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == CAR_ORIGIN_LATITUDE ) - assert sensor.attributes.get(ATTR_DESTINATION) == ",".join( - [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE] - ) - assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW" - assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S" - assert sensor.attributes.get(CONF_MODE) == mode - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is ( - traffic_mode == TRAFFIC_MODE_ENABLED + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == CAR_ORIGIN_LONGITUDE ) - assert sensor.attributes.get(ATTR_ICON) == icon + assert hass.states.get("sensor.test_origin").state == "22nd St NW" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == CAR_ORIGIN_LATITUDE + ) + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == CAR_ORIGIN_LONGITUDE + ) - # Test traffic mode disabled for vehicles - if mode in TRAVEL_MODES_VEHICLE: - assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( - ATTR_DURATION_IN_TRAFFIC - ) + assert hass.states.get("sensor.test_destination").state == "Service Rd S" + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LATITUDE) + == CAR_DESTINATION_LATITUDE + ) + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LONGITUDE) + == CAR_DESTINATION_LONGITUDE + ) @pytest.mark.usefixtures("valid_response") @@ -261,7 +258,9 @@ async def test_no_attribution(hass: HomeAssistant): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert hass.states.get("sensor.test").attributes.get(ATTR_ATTRIBUTION) is None + assert ( + hass.states.get("sensor.test_duration").attributes.get(ATTR_ATTRIBUTION) is None + ) async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): @@ -305,8 +304,7 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + assert hass.states.get("sensor.test_distance").state == "23.903" valid_response.assert_called_with( [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE], From 48c5aab5eebedd81e1dd887fa577d3e2950cdea3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 30 Jun 2022 08:16:05 +0200 Subject: [PATCH 013/820] Fix netgear method return type annotation (#74200) --- homeassistant/components/netgear/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index a4f8a4df14e..ae4186abbb5 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -250,7 +250,7 @@ class NetgearRouter: async with self._api_lock: await self.hass.async_add_executor_job(self._api.reboot) - async def async_check_new_firmware(self) -> None: + async def async_check_new_firmware(self) -> dict[str, Any] | None: """Check for new firmware of the router.""" async with self._api_lock: return await self.hass.async_add_executor_job(self._api.check_new_firmware) From 0cf922cc4e558d88b8093d417774d1fdf7993b4a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 29 Jun 2022 23:54:51 -0700 Subject: [PATCH 014/820] Allow legacy nest integration with no configuration.yaml (#74222) --- homeassistant/components/nest/__init__.py | 2 +- tests/components/nest/common.py | 12 +++++++++++- tests/components/nest/test_init_legacy.py | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 0e0128136ad..b31354b598c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -177,7 +177,7 @@ class SignalUpdateCallback: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from a config entry with dispatch between old/new flows.""" config_mode = config_flow.get_config_mode(hass) - if config_mode == config_flow.ConfigMode.LEGACY: + if DATA_SDM not in entry.data or config_mode == config_flow.ConfigMode.LEGACY: return await async_setup_legacy_entry(hass, entry) if config_mode == config_flow.ConfigMode.SDM: diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 765a954b6de..f86112ada75 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -147,7 +147,17 @@ TEST_CONFIG_LEGACY = NestTestConfig( }, }, }, - credential=None, +) +TEST_CONFIG_ENTRY_LEGACY = NestTestConfig( + config_entry_data={ + "auth_implementation": "local", + "tokens": { + "expires_at": time.time() + 86400, + "access_token": { + "token": "some-token", + }, + }, + }, ) diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py index cbf1bfe2d48..fc4e6070faf 100644 --- a/tests/components/nest/test_init_legacy.py +++ b/tests/components/nest/test_init_legacy.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest -from .common import TEST_CONFIG_LEGACY +from .common import TEST_CONFIG_ENTRY_LEGACY, TEST_CONFIG_LEGACY DOMAIN = "nest" @@ -33,6 +33,9 @@ def make_thermostat(): return device +@pytest.mark.parametrize( + "nest_test_config", [TEST_CONFIG_LEGACY, TEST_CONFIG_ENTRY_LEGACY] +) async def test_thermostat(hass, setup_base_platform): """Test simple initialization for thermostat entities.""" From 42d7f2a3b29fcb1e24bdde26546ff6120a9185db Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 30 Jun 2022 08:56:43 +0200 Subject: [PATCH 015/820] Update pylint to 2.14.4 (#74192) --- .../components/zha/core/channels/manufacturerspecific.py | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 943d13a57d6..b9f0ec1aaca 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -75,7 +75,7 @@ class OppleRemote(ZigbeeChannel): "trigger_indicator": True, } elif self.cluster.endpoint.model == "lumi.motion.ac01": - self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name + self.ZCL_INIT_ATTRS = { "presence": True, "monitoring_mode": True, "motion_sensitivity": True, diff --git a/requirements_test.txt b/requirements_test.txt index 6072ce896ee..3172c1f5544 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.961 pre-commit==2.19.0 -pylint==2.14.3 +pylint==2.14.4 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From 3a57f4363f9743c9a04e346e3dd85176eb7a7308 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 09:13:25 +0200 Subject: [PATCH 016/820] Revert "Patch out life360 entry setup in tests" (#74223) Revert "Patch out life360 entry setup in tests (#74212)" This reverts commit ab6e92f99696a8f5fab587dd56d07cc508c5b898. --- tests/components/life360/test_config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py index c24852fddf7..0b5b850ac23 100644 --- a/tests/components/life360/test_config_flow.py +++ b/tests/components/life360/test_config_flow.py @@ -118,7 +118,7 @@ async def test_user_show_form(hass, life360_api): assert keys[keys.index(key)].default == vol.UNDEFINED -async def test_user_config_flow_success(hass, life360_api, life360): +async def test_user_config_flow_success(hass, life360_api): """Test a successful user config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -228,7 +228,7 @@ async def test_reauth_config_flow_success(hass, life360_api, caplog, state): assert config_entry.data == TEST_CONFIG_DATA_2 -async def test_reauth_config_flow_login_error(hass, life360_api, life360, caplog): +async def test_reauth_config_flow_login_error(hass, life360_api, caplog): """Test a reauthorization config flow with a login error.""" config_entry = create_config_entry(hass) @@ -285,7 +285,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, life360, caplog # ========== Option flow Tests ========================================================= -async def test_options_flow(hass, life360): +async def test_options_flow(hass): """Test an options flow.""" config_entry = create_config_entry(hass) From 407da8c4b89b6ddd858fb0dc24719599f5650eca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 Jun 2022 09:42:15 +0200 Subject: [PATCH 017/820] Correct native_pressure_unit for zamg weather (#74225) --- homeassistant/components/zamg/weather.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index 2bf4a5b39f6..6910955fcf7 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_MILLIMETERS, + PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) @@ -87,9 +87,7 @@ def setup_platform( class ZamgWeather(WeatherEntity): """Representation of a weather condition.""" - _attr_native_pressure_unit = ( - LENGTH_MILLIMETERS # API reports l/m², equivalent to mm - ) + _attr_native_pressure_unit = PRESSURE_HPA _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR From fdb7a23171fd5cc9f18a0fef0294c95f1a0bbc31 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 10:08:06 +0200 Subject: [PATCH 018/820] Update black to 22.6.0 (#74209) Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18b34a222aa..d6400e113c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 0d204771e40..23d675afaf1 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.4 -black==22.3.0 +black==22.6.0 codespell==2.1.0 flake8-comprehensions==3.8.0 flake8-docstrings==1.6.0 From 57fd84e20c9e98df52a6e81af1fa84ee86028aa8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 15:34:48 +0200 Subject: [PATCH 019/820] Improve type hints in demo (#74236) --- .../components/demo/binary_sensor.py | 8 +- homeassistant/components/demo/calendar.py | 15 ++- homeassistant/components/demo/camera.py | 8 +- homeassistant/components/demo/climate.py | 96 ++++++++++--------- homeassistant/components/demo/config_flow.py | 9 +- homeassistant/components/demo/light.py | 19 ++-- 6 files changed, 82 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index bf2731d3d72..f2b759a9cf3 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -73,7 +73,7 @@ class DemoBinarySensor(BinarySensorEntity): ) @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id.""" return self._unique_id @@ -83,16 +83,16 @@ class DemoBinarySensor(BinarySensorEntity): return self._sensor_type @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo binary sensor.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the binary sensor.""" return self._name @property - def is_on(self): + def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self._state diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 3a8b909bd0c..f8a803ba860 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -72,7 +72,12 @@ class DemoCalendar(CalendarEntity): """Return the name of the entity.""" return self._name - async def async_get_events(self, hass, start_date, end_date) -> list[CalendarEvent]: + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime.datetime, + end_date: datetime.datetime, + ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" return [self._event] @@ -80,15 +85,15 @@ class DemoCalendar(CalendarEntity): class LegacyDemoCalendar(CalendarEventDevice): """Calendar for exercising shim API.""" - def __init__(self, name): + def __init__(self, name: str) -> None: """Initialize demo calendar.""" self._name = name - one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30) + one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) self._event = { "start": {"dateTime": one_hour_from_now.isoformat()}, "end": { "dateTime": ( - one_hour_from_now + dt_util.dt.timedelta(minutes=60) + one_hour_from_now + datetime.timedelta(minutes=60) ).isoformat() }, "summary": "Future Event", @@ -102,7 +107,7 @@ class LegacyDemoCalendar(CalendarEventDevice): return self._event @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 25026bce11b..b8e0a714253 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -58,23 +58,23 @@ class DemoCamera(Camera): return await self.hass.async_add_executor_job(image_path.read_bytes) - async def async_enable_motion_detection(self): + async def async_enable_motion_detection(self) -> None: """Enable the Motion detection in base station (Arm).""" self._attr_motion_detection_enabled = True self.async_write_ha_state() - async def async_disable_motion_detection(self): + async def async_disable_motion_detection(self) -> None: """Disable the motion detection in base station (Disarm).""" self._attr_motion_detection_enabled = False self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off camera.""" self._attr_is_streaming = False self._attr_is_on = False self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on camera.""" self._attr_is_streaming = True self._attr_is_on = True diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index a4d6ca6da07..ae633c5937a 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,6 +1,8 @@ """Demo platform that offers a fake climate device.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, @@ -103,24 +105,24 @@ class DemoClimate(ClimateEntity): def __init__( self, - unique_id, - name, - target_temperature, - unit_of_measurement, - preset, - current_temperature, - fan_mode, - target_humidity, - current_humidity, - swing_mode, - hvac_mode, - hvac_action, - aux, - target_temp_high, - target_temp_low, - hvac_modes, - preset_modes=None, - ): + unique_id: str, + name: str, + target_temperature: float | None, + unit_of_measurement: str, + preset: str | None, + current_temperature: float, + fan_mode: str | None, + target_humidity: int | None, + current_humidity: int | None, + swing_mode: str | None, + hvac_mode: HVACMode, + hvac_action: HVACAction | None, + aux: bool | None, + target_temp_high: float | None, + target_temp_low: float | None, + hvac_modes: list[HVACMode], + preset_modes: list[str] | None = None, + ) -> None: """Initialize the climate device.""" self._unique_id = unique_id self._name = name @@ -175,111 +177,111 @@ class DemoClimate(ClimateEntity): ) @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id.""" return self._unique_id @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return self._support_flags @property - def should_poll(self): + def should_poll(self) -> bool: """Return the polling state.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the climate device.""" return self._name @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self._current_temperature @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._target_temperature @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" return self._target_temperature_high @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return self._target_temperature_low @property - def current_humidity(self): + def current_humidity(self) -> int | None: """Return the current humidity.""" return self._current_humidity @property - def target_humidity(self): + def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" return self._target_humidity @property - def hvac_action(self): + def hvac_action(self) -> HVACAction | None: """Return current operation ie. heat, cool, idle.""" return self._hvac_action @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode: """Return hvac target hvac state.""" return self._hvac_mode @property - def hvac_modes(self): + def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" return self._hvac_modes @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return preset mode.""" return self._preset @property - def preset_modes(self): + def preset_modes(self) -> list[str] | None: """Return preset modes.""" return self._preset_modes @property - def is_aux_heat(self): + def is_aux_heat(self) -> bool | None: """Return true if aux heat is on.""" return self._aux @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the fan setting.""" return self._current_fan_mode @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return self._fan_modes @property - def swing_mode(self): + def swing_mode(self) -> str | None: """Return the swing setting.""" return self._current_swing_mode @property - def swing_modes(self): + def swing_modes(self) -> list[str]: """List of available swing modes.""" return self._swing_modes - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if kwargs.get(ATTR_TEMPERATURE) is not None: self._target_temperature = kwargs.get(ATTR_TEMPERATURE) @@ -291,37 +293,37 @@ class DemoClimate(ClimateEntity): self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) self.async_write_ha_state() - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Set new humidity level.""" self._target_humidity = humidity self.async_write_ha_state() - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" self._current_swing_mode = swing_mode self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" self._current_fan_mode = fan_mode self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" self._hvac_mode = hvac_mode self.async_write_ha_state() - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Update preset_mode on.""" self._preset = preset_mode self.async_write_ha_state() - async def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" self._aux = True self.async_write_ha_state() - async def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" self._aux = False self.async_write_ha_state() diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index e389574c658..0163123b578 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import DOMAIN @@ -29,7 +30,7 @@ class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, import_info): + async def async_step_import(self, import_info) -> FlowResult: """Set the config entry up from yaml.""" return self.async_create_entry(title="Demo", data={}) @@ -42,11 +43,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.config_entry = config_entry self.options = dict(config_entry.options) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> FlowResult: """Manage the options.""" return await self.async_step_options_1() - async def async_step_options_1(self, user_input=None): + async def async_step_options_1(self, user_input=None) -> FlowResult: """Manage the options.""" if user_input is not None: self.options.update(user_input) @@ -69,7 +70,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def async_step_options_2(self, user_input=None): + async def async_step_options_2(self, user_input=None) -> FlowResult: """Manage the options 2.""" if user_input is not None: self.options.update(user_input) diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index a9fc6cf2044..00e5fd4def1 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import random +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -107,18 +108,18 @@ class DemoLight(LightEntity): def __init__( self, - unique_id, - name, + unique_id: str, + name: str, state, available=False, brightness=180, ct=None, # pylint: disable=invalid-name - effect_list=None, + effect_list: list[str] | None = None, effect=None, hs_color=None, rgbw_color=None, rgbww_color=None, - supported_color_modes=None, + supported_color_modes: set[ColorMode] | None = None, ): """Initialize the light.""" self._available = True @@ -169,7 +170,7 @@ class DemoLight(LightEntity): return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for light.""" return self._unique_id @@ -211,7 +212,7 @@ class DemoLight(LightEntity): return self._ct @property - def effect_list(self) -> list: + def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" return self._effect_list @@ -231,11 +232,11 @@ class DemoLight(LightEntity): return self._features @property - def supported_color_modes(self) -> set | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" return self._color_modes - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" self._state = True @@ -269,7 +270,7 @@ class DemoLight(LightEntity): # Home Assistant about updates in our state ourselves. self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._state = False From f5c6a6be3a0c085c016c3be31985f6bbfd185fc8 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Fri, 1 Jul 2022 02:13:08 +1200 Subject: [PATCH 020/820] Add config flow to AnthemAV integration (#53268) * Add config flow to AnthemAV integration * Add importing of existing configuration * Change setting to optional and add default value * Use entity attribute * Reduce changes by removing additional media player properties * Remove title from translation * Refactor config flow and fix PR comments * Fix a failing test because of wrong renaming * Add typing and use existing class and enum * Bump dependency to v1.3.1 * Remove unecessary async_reload_entry * Fix requirements_test_all after rebase * Add const for timeout and remove async_block in test * Reapply CodeOwner and configflow after rebase * Remove name from configflow * Fix manifest prettier failure * Simplify code and avoid catching broad exception * Removed unused strings and translations * Avoid asserting hass.data --- CODEOWNERS | 2 + homeassistant/components/anthemav/__init__.py | 60 ++++++++- .../components/anthemav/config_flow.py | 96 ++++++++++++++ homeassistant/components/anthemav/const.py | 7 ++ .../components/anthemav/manifest.json | 5 +- .../components/anthemav/media_player.py | 117 ++++++++++-------- .../components/anthemav/strings.json | 19 +++ .../components/anthemav/translations/en.json | 19 +++ .../components/anthemav/translations/fr.json | 19 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/anthemav/__init__.py | 1 + tests/components/anthemav/conftest.py | 28 +++++ tests/components/anthemav/test_config_flow.py | 115 +++++++++++++++++ tests/components/anthemav/test_init.py | 65 ++++++++++ 16 files changed, 506 insertions(+), 53 deletions(-) create mode 100644 homeassistant/components/anthemav/config_flow.py create mode 100644 homeassistant/components/anthemav/const.py create mode 100644 homeassistant/components/anthemav/strings.json create mode 100644 homeassistant/components/anthemav/translations/en.json create mode 100644 homeassistant/components/anthemav/translations/fr.json create mode 100644 tests/components/anthemav/__init__.py create mode 100644 tests/components/anthemav/conftest.py create mode 100644 tests/components/anthemav/test_config_flow.py create mode 100644 tests/components/anthemav/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 1b6d88b5464..ed4ab888541 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -74,6 +74,8 @@ build.json @home-assistant/supervisor /tests/components/analytics/ @home-assistant/core @ludeeus /homeassistant/components/androidtv/ @JeffLIrion @ollo69 /tests/components/androidtv/ @JeffLIrion @ollo69 +/homeassistant/components/anthemav/ @hyralex +/tests/components/anthemav/ @hyralex /homeassistant/components/apache_kafka/ @bachya /tests/components/apache_kafka/ @bachya /homeassistant/components/api/ @home-assistant/core diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index 56b06e865c2..5ad845b52d6 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -1 +1,59 @@ -"""The anthemav component.""" +"""The Anthem A/V Receivers integration.""" +from __future__ import annotations + +import logging + +import anthemav + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ANTHEMAV_UDATE_SIGNAL, DOMAIN + +PLATFORMS = [Platform.MEDIA_PLAYER] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Anthem A/V Receivers from a config entry.""" + + @callback + def async_anthemav_update_callback(message): + """Receive notification from transport that new data exists.""" + _LOGGER.debug("Received update callback from AVR: %s", message) + async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.data[CONF_NAME]}") + + try: + avr = await anthemav.Connection.create( + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + update_callback=async_anthemav_update_callback, + ) + + except OSError as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + avr = hass.data[DOMAIN][entry.entry_id] + + if avr is not None: + _LOGGER.debug("Close avr connection") + avr.close() + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/anthemav/config_flow.py b/homeassistant/components/anthemav/config_flow.py new file mode 100644 index 00000000000..55e1fd42e07 --- /dev/null +++ b/homeassistant/components/anthemav/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow for Anthem A/V Receivers integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import anthemav +from anthemav.connection import Connection +from anthemav.device_error import DeviceError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac + +from .const import CONF_MODEL, DEFAULT_NAME, DEFAULT_PORT, DOMAIN + +DEVICE_TIMEOUT_SECONDS = 4.0 + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + + +async def connect_device(user_input: dict[str, Any]) -> Connection: + """Connect to the AVR device.""" + avr = await anthemav.Connection.create( + host=user_input[CONF_HOST], port=user_input[CONF_PORT], auto_reconnect=False + ) + await avr.reconnect() + await avr.protocol.wait_for_device_initialised(DEVICE_TIMEOUT_SECONDS) + return avr + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Anthem A/V Receivers.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + if CONF_NAME not in user_input: + user_input[CONF_NAME] = DEFAULT_NAME + + errors = {} + + avr: Connection | None = None + try: + avr = await connect_device(user_input) + except OSError: + _LOGGER.error( + "Couldn't establish connection to %s:%s", + user_input[CONF_HOST], + user_input[CONF_PORT], + ) + errors["base"] = "cannot_connect" + except DeviceError: + _LOGGER.error( + "Couldn't receive device information from %s:%s", + user_input[CONF_HOST], + user_input[CONF_PORT], + ) + errors["base"] = "cannot_receive_deviceinfo" + else: + user_input[CONF_MAC] = format_mac(avr.protocol.macaddress) + user_input[CONF_MODEL] = avr.protocol.model + await self.async_set_unique_id(user_input[CONF_MAC]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + finally: + if avr is not None: + avr.close() + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/anthemav/const.py b/homeassistant/components/anthemav/const.py new file mode 100644 index 00000000000..c63a477f7f9 --- /dev/null +++ b/homeassistant/components/anthemav/const.py @@ -0,0 +1,7 @@ +"""Constants for the Anthem A/V Receivers integration.""" +ANTHEMAV_UDATE_SIGNAL = "anthemav_update" +CONF_MODEL = "model" +DEFAULT_NAME = "Anthem AV" +DEFAULT_PORT = 14999 +DOMAIN = "anthemav" +MANUFACTURER = "Anthem" diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index c43b976416d..33fe565ad03 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -2,8 +2,9 @@ "domain": "anthemav", "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", - "requirements": ["anthemav==1.2.0"], - "codeowners": [], + "requirements": ["anthemav==1.3.2"], + "codeowners": ["@hyralex"], + "config_flow": true, "iot_class": "local_push", "loggers": ["anthemav"] } diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 05bfea7ef45..93fdedef054 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -2,8 +2,9 @@ from __future__ import annotations import logging +from typing import Any -import anthemav +from anthemav.connection import Connection import voluptuous as vol from homeassistant.components.media_player import ( @@ -11,33 +12,37 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, + CONF_MAC, CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import ( + ANTHEMAV_UDATE_SIGNAL, + CONF_MODEL, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, + MANUFACTURER, +) + _LOGGER = logging.getLogger(__name__) -DOMAIN = "anthemav" - -DEFAULT_PORT = 14999 - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, } ) @@ -50,30 +55,33 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up our socket to the AVR.""" - - host = config[CONF_HOST] - port = config[CONF_PORT] - name = config.get(CONF_NAME) - device = None - - _LOGGER.info("Provisioning Anthem AVR device at %s:%d", host, port) - - @callback - def async_anthemav_update_callback(message): - """Receive notification from transport that new data exists.""" - _LOGGER.debug("Received update callback from AVR: %s", message) - async_dispatcher_send(hass, DOMAIN) - - avr = await anthemav.Connection.create( - host=host, port=port, update_callback=async_anthemav_update_callback + _LOGGER.warning( + "AnthemAV configuration is deprecated and has been automatically imported. Please remove the integration from your configuration file" + ) + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - device = AnthemAVR(avr, name) + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entry.""" + name = config_entry.data[CONF_NAME] + macaddress = config_entry.data[CONF_MAC] + model = config_entry.data[CONF_MODEL] + + avr = hass.data[DOMAIN][config_entry.entry_id] + + device = AnthemAVR(avr, name, macaddress, model) _LOGGER.debug("dump_devicedata: %s", device.dump_avrdata) _LOGGER.debug("dump_conndata: %s", avr.dump_conndata) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.avr.close) async_add_entities([device]) @@ -89,23 +97,34 @@ class AnthemAVR(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, avr, name): + def __init__(self, avr: Connection, name: str, macaddress: str, model: str) -> None: """Initialize entity with transport.""" super().__init__() self.avr = avr - self._attr_name = name or self._lookup("model") + self._attr_name = name + self._attr_unique_id = macaddress + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, macaddress)}, + name=name, + manufacturer=MANUFACTURER, + model=model, + ) - def _lookup(self, propname, dval=None): + def _lookup(self, propname: str, dval: Any | None = None) -> Any | None: return getattr(self.avr.protocol, propname, dval) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self.async_on_remove( - async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) + async_dispatcher_connect( + self.hass, + f"{ANTHEMAV_UDATE_SIGNAL}_{self._attr_name}", + self.async_write_ha_state, + ) ) @property - def state(self): + def state(self) -> str | None: """Return state of power on/off.""" pwrstate = self._lookup("power") @@ -116,22 +135,22 @@ class AnthemAVR(MediaPlayerEntity): return None @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool | None: """Return boolean reflecting mute state on device.""" return self._lookup("mute", False) @property - def volume_level(self): + def volume_level(self) -> float | None: """Return volume level from 0 to 1.""" return self._lookup("volume_as_percentage", 0.0) @property - def media_title(self): + def media_title(self) -> str | None: """Return current input name (closest we have to media title).""" return self._lookup("input_name", "No Source") @property - def app_name(self): + def app_name(self) -> str | None: """Return details about current video and audio stream.""" return ( f"{self._lookup('video_input_resolution_text', '')} " @@ -139,38 +158,38 @@ class AnthemAVR(MediaPlayerEntity): ) @property - def source(self): + def source(self) -> str | None: """Return currently selected input.""" return self._lookup("input_name", "Unknown") @property - def source_list(self): + def source_list(self) -> list[str] | None: """Return all active, configured inputs.""" return self._lookup("input_list", ["Unknown"]) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Change AVR to the designated source (by name).""" self._update_avr("input_name", source) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn AVR power off.""" self._update_avr("power", False) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn AVR power on.""" self._update_avr("power", True) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set AVR volume (0 to 1).""" self._update_avr("volume_as_percentage", volume) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Engage AVR mute.""" self._update_avr("mute", mute) - def _update_avr(self, propname, value): + def _update_avr(self, propname: str, value: Any | None) -> None: """Update a property in the AVR.""" - _LOGGER.info("Sending command to AVR: set %s to %s", propname, str(value)) + _LOGGER.debug("Sending command to AVR: set %s to %s", propname, str(value)) setattr(self.avr.protocol, propname, value) @property diff --git a/homeassistant/components/anthemav/strings.json b/homeassistant/components/anthemav/strings.json new file mode 100644 index 00000000000..1f1dd0ec75b --- /dev/null +++ b/homeassistant/components/anthemav/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_receive_deviceinfo": "Failed to retreive MAC Address. Make sure the device is turned on" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/anthemav/translations/en.json b/homeassistant/components/anthemav/translations/en.json new file mode 100644 index 00000000000..9177d5a6e70 --- /dev/null +++ b/homeassistant/components/anthemav/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "cannot_receive_deviceinfo": "Failed to retreive MAC Address. Make sure the device is turned on" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/fr.json b/homeassistant/components/anthemav/translations/fr.json new file mode 100644 index 00000000000..cf1e7c7aded --- /dev/null +++ b/homeassistant/components/anthemav/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "cannot_receive_deviceinfo": "Erreur lors de la d\u00e9couverte de l'addresse MAC. V\u00e9rifiez que l'appareil est allum\u00e9." + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3c6ad94a21f..f344e3acb5c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -29,6 +29,7 @@ FLOWS = { "ambiclimate", "ambient_station", "androidtv", + "anthemav", "apple_tv", "arcam_fmj", "aseko_pool_live", diff --git a/requirements_all.txt b/requirements_all.txt index 98a6c6f5a95..f992d73f894 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -310,7 +310,7 @@ androidtv[async]==0.0.67 anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anthemav -anthemav==1.2.0 +anthemav==1.3.2 # homeassistant.components.apcupsd apcaccess==0.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64ad3f769b9..12e59e5d423 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,6 +269,9 @@ ambiclimate==0.2.1 # homeassistant.components.androidtv androidtv[async]==0.0.67 +# homeassistant.components.anthemav +anthemav==1.3.2 + # homeassistant.components.apprise apprise==0.9.9 diff --git a/tests/components/anthemav/__init__.py b/tests/components/anthemav/__init__.py new file mode 100644 index 00000000000..829f99b10b5 --- /dev/null +++ b/tests/components/anthemav/__init__.py @@ -0,0 +1 @@ +"""Tests for the Anthem A/V Receivers integration.""" diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py new file mode 100644 index 00000000000..8fbdf3145c3 --- /dev/null +++ b/tests/components/anthemav/conftest.py @@ -0,0 +1,28 @@ +"""Fixtures for anthemav integration tests.""" +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + + +@pytest.fixture +def mock_anthemav() -> AsyncMock: + """Return the default mocked anthemav.""" + avr = AsyncMock() + avr.protocol.macaddress = "000000000001" + avr.protocol.model = "MRX 520" + avr.reconnect = AsyncMock() + avr.close = MagicMock() + avr.protocol.input_list = [] + avr.protocol.audio_listening_mode_list = [] + return avr + + +@pytest.fixture +def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock: + """Return the default mocked connection.create.""" + + with patch( + "anthemav.Connection.create", + return_value=mock_anthemav, + ) as mock: + yield mock diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py new file mode 100644 index 00000000000..98ed0f0abf0 --- /dev/null +++ b/tests/components/anthemav/test_config_flow.py @@ -0,0 +1,115 @@ +"""Test the Anthem A/V Receivers config flow.""" +from unittest.mock import AsyncMock, patch + +from anthemav.device_error import DeviceError + +from homeassistant import config_entries +from homeassistant.components.anthemav.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_form_with_valid_connection( + hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.anthemav.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 14999, + }, + ) + + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + "host": "1.1.1.1", + "port": 14999, + "name": "Anthem AV", + "mac": "00:00:00:00:00:01", + "model": "MRX 520", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_device_info_error(hass: HomeAssistant) -> None: + """Test we handle DeviceError from library.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "anthemav.Connection.create", + side_effect=DeviceError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 14999, + }, + ) + + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_receive_deviceinfo"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "anthemav.Connection.create", + side_effect=OSError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 14999, + }, + ) + + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_import_configuration( + hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock +) -> None: + """Test we import existing configuration.""" + config = { + "host": "1.1.1.1", + "port": 14999, + "name": "Anthem Av Import", + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "host": "1.1.1.1", + "port": 14999, + "name": "Anthem Av Import", + "mac": "00:00:00:00:00:01", + "model": "MRX 520", + } diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py new file mode 100644 index 00000000000..866925f4e46 --- /dev/null +++ b/tests/components/anthemav/test_init.py @@ -0,0 +1,65 @@ +"""Test the Anthem A/V Receivers config flow.""" +from unittest.mock import ANY, AsyncMock, patch + +from homeassistant import config_entries +from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock +) -> None: + """Test load and unload AnthemAv component.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 14999, + CONF_NAME: "Anthem AV", + CONF_MAC: "aabbccddeeff", + CONF_MODEL: "MRX 520", + }, + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # assert avr is created + mock_connection_create.assert_called_with( + host="1.1.1.1", port=14999, update_callback=ANY + ) + assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + + # unload + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + # assert unload and avr is closed + assert mock_config_entry.state == config_entries.ConfigEntryState.NOT_LOADED + mock_anthemav.close.assert_called_once() + + +async def test_config_entry_not_ready(hass: HomeAssistant) -> None: + """Test AnthemAV configuration entry not ready.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 14999, + CONF_NAME: "Anthem AV", + CONF_MAC: "aabbccddeeff", + CONF_MODEL: "MRX 520", + }, + ) + + with patch( + "anthemav.Connection.create", + side_effect=OSError, + ): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY From d38e8e213a0434f53dd043ac2c9976c761dd9476 Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 30 Jun 2022 09:35:06 -0600 Subject: [PATCH 021/820] Fix intellifire climate control not needing a default fireplace (#74253) --- homeassistant/components/intellifire/climate.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py index 37a126bf3d6..a00a20f64f1 100644 --- a/homeassistant/components/intellifire/climate.py +++ b/homeassistant/components/intellifire/climate.py @@ -80,7 +80,6 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity): (raw_target_temp * 9 / 5) + 32, ) await self.coordinator.control_api.set_thermostat_c( - fireplace=self.coordinator.control_api.default_fireplace, temp_c=self.last_temp, ) @@ -101,20 +100,15 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity): ) if hvac_mode == HVACMode.OFF: - await self.coordinator.control_api.turn_off_thermostat( - fireplace=self.coordinator.control_api.default_fireplace - ) + await self.coordinator.control_api.turn_off_thermostat() return # hvac_mode == HVACMode.HEAT # 1) Set the desired target temp await self.coordinator.control_api.set_thermostat_c( - fireplace=self.coordinator.control_api.default_fireplace, temp_c=self.last_temp, ) # 2) Make sure the fireplace is on! if not self.coordinator.read_api.data.is_on: - await self.coordinator.control_api.flame_on( - fireplace=self.coordinator.control_api.default_fireplace, - ) + await self.coordinator.control_api.flame_on() From de700e7859eb2251c6a90f128aef5278d3be38a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 30 Jun 2022 18:59:46 +0200 Subject: [PATCH 022/820] Make media_player.toggle turn on a standby device (#74221) --- homeassistant/components/media_player/__init__.py | 3 ++- .../components/media_player/test_async_helpers.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index dc2f3624a0e..14546a36ec8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -52,6 +52,7 @@ from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PLAYING, + STATE_STANDBY, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -888,7 +889,7 @@ class MediaPlayerEntity(Entity): await self.hass.async_add_executor_job(self.toggle) return - if self.state in (STATE_OFF, STATE_IDLE): + if self.state in (STATE_OFF, STATE_IDLE, STATE_STANDBY): await self.async_turn_on() else: await self.async_turn_off() diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 53c80bfc8de..8be263e7ee0 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -8,6 +8,7 @@ from homeassistant.const import ( STATE_ON, STATE_PAUSED, STATE_PLAYING, + STATE_STANDBY, ) @@ -79,9 +80,13 @@ class ExtendedMediaPlayer(mp.MediaPlayerEntity): """Turn off state.""" self._state = STATE_OFF + def standby(self): + """Put device in standby.""" + self._state = STATE_STANDBY + def toggle(self): """Toggle the power on the media player.""" - if self._state in [STATE_OFF, STATE_IDLE]: + if self._state in [STATE_OFF, STATE_IDLE, STATE_STANDBY]: self._state = STATE_ON else: self._state = STATE_OFF @@ -138,6 +143,10 @@ class SimpleMediaPlayer(mp.MediaPlayerEntity): """Turn off state.""" self._state = STATE_OFF + def standby(self): + """Put device in standby.""" + self._state = STATE_STANDBY + @pytest.fixture(params=[ExtendedMediaPlayer, SimpleMediaPlayer]) def player(hass, request): @@ -188,3 +197,7 @@ async def test_toggle(player): assert player.state == STATE_ON await player.async_toggle() assert player.state == STATE_OFF + player.standby() + assert player.state == STATE_STANDBY + await player.async_toggle() + assert player.state == STATE_ON From 7573dc34aa1ee8a8949d2306c37b59aaa40ff345 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jun 2022 10:00:10 -0700 Subject: [PATCH 023/820] Treat thermostat unknown state like unavailable in alexa (#74220) --- homeassistant/components/alexa/capabilities.py | 2 ++ tests/components/alexa/test_capabilities.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 818b4b794cf..25ec43b689c 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1046,6 +1046,8 @@ class AlexaThermostatController(AlexaCapability): if preset in API_THERMOSTAT_PRESETS: mode = API_THERMOSTAT_PRESETS[preset] + elif self.entity.state == STATE_UNKNOWN: + return None else: mode = API_THERMOSTAT_MODES.get(self.entity.state) if mode is None: diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 3e176b0fb8c..ea6c96bbaef 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -658,13 +658,16 @@ async def test_report_climate_state(hass): "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) - hass.states.async_set( - "climate.unavailable", - "unavailable", - {"friendly_name": "Climate Unavailable", "supported_features": 91}, - ) - properties = await reported_properties(hass, "climate.unavailable") - properties.assert_not_has_property("Alexa.ThermostatController", "thermostatMode") + for state in "unavailable", "unknown": + hass.states.async_set( + f"climate.{state}", + state, + {"friendly_name": f"Climate {state}", "supported_features": 91}, + ) + properties = await reported_properties(hass, f"climate.{state}") + properties.assert_not_has_property( + "Alexa.ThermostatController", "thermostatMode" + ) hass.states.async_set( "climate.unsupported", From 781e4571b27b6aaaabd885e7af53cbf2465b41f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 19:00:34 +0200 Subject: [PATCH 024/820] Add CalendarEntity checks to pylint plugin (#74228) * Add CalendarEntity checks to pylint plugin * adjust air_quality ignore * Mark state property as final --- homeassistant/components/calendar/__init__.py | 6 +++-- pylint/plugins/hass_enforce_type_hints.py | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 432b6943473..4623f490302 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -222,8 +222,9 @@ class CalendarEventDevice(Entity): "description": event["description"], } + @final @property - def state(self) -> str | None: + def state(self) -> str: """Return the state of the calendar event.""" if (event := self.event) is None: return STATE_OFF @@ -276,8 +277,9 @@ class CalendarEntity(Entity): "description": event.description if event.description else "", } + @final @property - def state(self) -> str | None: + def state(self) -> str: """Return the state of the calendar event.""" if (event := self.event) is None: return STATE_OFF diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 5f4f641cbae..3ac072373b8 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -591,6 +591,7 @@ _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ ), ] _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { + # "air_quality": [], # ignored as deprecated "alarm_control_panel": [ ClassTypeHintMatch( base_class="Entity", @@ -717,6 +718,30 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "calendar": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="CalendarEntity", + matches=[ + TypeHintMatch( + function_name="event", + return_type=["CalendarEvent", None], + ), + TypeHintMatch( + function_name="async_get_events", + arg_types={ + 1: "HomeAssistant", + 2: "datetime", + 3: "datetime", + }, + return_type="list[CalendarEvent]", + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", From 5fa3b90b2ca309cab3ee03d2a27bf6aaa4dc5a68 Mon Sep 17 00:00:00 2001 From: MasonCrawford Date: Fri, 1 Jul 2022 01:00:39 +0800 Subject: [PATCH 025/820] Add config flow to lg_soundbar (#71153) Co-authored-by: Paulus Schoutsen --- .../components/discovery/__init__.py | 2 +- .../components/lg_soundbar/__init__.py | 37 ++++++++ .../components/lg_soundbar/config_flow.py | 78 +++++++++++++++ homeassistant/components/lg_soundbar/const.py | 4 + .../components/lg_soundbar/manifest.json | 3 +- .../components/lg_soundbar/media_player.py | 51 +++++----- .../components/lg_soundbar/strings.json | 18 ++++ .../lg_soundbar/translations/en.json | 18 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/lg_soundbar/__init__.py | 1 + .../lg_soundbar/test_config_flow.py | 95 +++++++++++++++++++ 13 files changed, 284 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/lg_soundbar/config_flow.py create mode 100644 homeassistant/components/lg_soundbar/const.py create mode 100644 homeassistant/components/lg_soundbar/strings.json create mode 100644 homeassistant/components/lg_soundbar/translations/en.json create mode 100644 tests/components/lg_soundbar/__init__.py create mode 100644 tests/components/lg_soundbar/test_config_flow.py diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index a0ffbf235ab..3c3538c1ca0 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -63,7 +63,6 @@ SERVICE_HANDLERS = { "openhome": ServiceDetails("media_player", "openhome"), "bose_soundtouch": ServiceDetails("media_player", "soundtouch"), "bluesound": ServiceDetails("media_player", "bluesound"), - "lg_smart_device": ServiceDetails("media_player", "lg_soundbar"), } OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {} @@ -98,6 +97,7 @@ MIGRATED_SERVICE_HANDLERS = [ SERVICE_YEELIGHT, SERVICE_SABNZBD, "nanoleaf_aurora", + "lg_smart_device", ] DEFAULT_ENABLED = ( diff --git a/homeassistant/components/lg_soundbar/__init__.py b/homeassistant/components/lg_soundbar/__init__.py index 175153556f9..75b2109b22a 100644 --- a/homeassistant/components/lg_soundbar/__init__.py +++ b/homeassistant/components/lg_soundbar/__init__.py @@ -1 +1,38 @@ """The lg_soundbar component.""" +import logging + +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.exceptions import ConfigEntryNotReady + +from .config_flow import test_connect +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +async def async_setup_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Set up platform from a ConfigEntry.""" + hass.data.setdefault(DOMAIN, {}) + # Verify the device is reachable with the given config before setting up the platform + try: + await hass.async_add_executor_job( + test_connect, entry.data[CONF_HOST], entry.data[CONF_PORT] + ) + except ConnectionError as err: + raise ConfigEntryNotReady from err + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Unload a config entry.""" + result = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return result diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py new file mode 100644 index 00000000000..bd9a727d1f4 --- /dev/null +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow to configure the LG Soundbar integration.""" +from queue import Queue +import socket + +import temescal +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DEFAULT_PORT, DOMAIN + +DATA_SCHEMA = { + vol.Required(CONF_HOST): str, +} + + +def test_connect(host, port): + """LG Soundbar config flow test_connect.""" + uuid_q = Queue(maxsize=1) + name_q = Queue(maxsize=1) + + def msg_callback(response): + if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]: + uuid_q.put_nowait(response["data"]["s_uuid"]) + if ( + response["msg"] == "SPK_LIST_VIEW_INFO" + and "s_user_name" in response["data"] + ): + name_q.put_nowait(response["data"]["s_user_name"]) + + try: + connection = temescal.temescal(host, port=port, callback=msg_callback) + connection.get_mac_info() + connection.get_info() + details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)} + return details + except socket.timeout as err: + raise ConnectionError(f"Connection timeout with server: {host}:{port}") from err + except OSError as err: + raise ConnectionError(f"Cannot resolve hostname: {host}") from err + + +class LGSoundbarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """LG Soundbar config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_form() + + errors = {} + try: + details = await self.hass.async_add_executor_job( + test_connect, user_input[CONF_HOST], DEFAULT_PORT + ) + except ConnectionError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(details["uuid"]) + self._abort_if_unique_id_configured() + info = { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: DEFAULT_PORT, + } + return self.async_create_entry(title=details["name"], data=info) + + return self._show_form(errors) + + def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(DATA_SCHEMA), + errors=errors if errors else {}, + ) diff --git a/homeassistant/components/lg_soundbar/const.py b/homeassistant/components/lg_soundbar/const.py new file mode 100644 index 00000000000..c71e43c0d60 --- /dev/null +++ b/homeassistant/components/lg_soundbar/const.py @@ -0,0 +1,4 @@ +"""Constants for the LG Soundbar integration.""" +DOMAIN = "lg_soundbar" + +DEFAULT_PORT = 9741 diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index f40ad1d194c..c05174a8938 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -1,8 +1,9 @@ { "domain": "lg_soundbar", + "config_flow": true, "name": "LG Soundbars", "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", - "requirements": ["temescal==0.3"], + "requirements": ["temescal==0.5"], "codeowners": [], "iot_class": "local_polling", "loggers": ["temescal"] diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 569678c8c15..f8f6fcf26fd 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -7,26 +7,33 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) -from homeassistant.const import STATE_ON +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the LG platform.""" - if discovery_info is not None: - add_entities([LGDevice(discovery_info)]) + """Set up media_player from a config entry created in the integrations UI.""" + async_add_entities( + [ + LGDevice( + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], + config_entry.unique_id, + ) + ] + ) class LGDevice(MediaPlayerEntity): """Representation of an LG soundbar device.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -34,13 +41,13 @@ class LGDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOUND_MODE ) - def __init__(self, discovery_info): + def __init__(self, host, port, unique_id): """Initialize the LG speakers.""" - self._host = discovery_info["host"] - self._port = discovery_info["port"] - self._hostname = discovery_info["hostname"] + self._host = host + self._port = port + self._attr_unique_id = unique_id - self._name = self._hostname.split(".")[0] + self._name = None self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -68,6 +75,8 @@ class LGDevice(MediaPlayerEntity): self._device = temescal.temescal( self._host, port=self._port, callback=self.handle_event ) + self._device.get_product_info() + self._device.get_mac_info() self.update() def handle_event(self, response): @@ -116,7 +125,8 @@ class LGDevice(MediaPlayerEntity): if "i_curr_eq" in data: self._equaliser = data["i_curr_eq"] if "s_user_name" in data: - self._name = data["s_user_name"] + self._attr_name = data["s_user_name"] + self.schedule_update_ha_state() def update(self): @@ -125,17 +135,6 @@ class LGDevice(MediaPlayerEntity): self._device.get_info() self._device.get_func() self._device.get_settings() - self._device.get_product_info() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name @property def volume_level(self): diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json new file mode 100644 index 00000000000..ef7bf32a051 --- /dev/null +++ b/homeassistant/components/lg_soundbar/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "existing_instance_updated": "Updated existing configuration.", + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json new file mode 100644 index 00000000000..a646279203f --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "existing_instance_updated": "Updated existing configuration." + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f344e3acb5c..b0da8f79418 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -190,6 +190,7 @@ FLOWS = { "kulersky", "launch_library", "laundrify", + "lg_soundbar", "life360", "lifx", "litejet", diff --git a/requirements_all.txt b/requirements_all.txt index f992d73f894..d437757bacd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2289,7 +2289,7 @@ tellcore-py==1.1.2 tellduslive==0.10.11 # homeassistant.components.lg_soundbar -temescal==0.3 +temescal==0.5 # homeassistant.components.temper temperusb==1.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12e59e5d423..e29624cd2f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1524,6 +1524,9 @@ tailscale==0.2.0 # homeassistant.components.tellduslive tellduslive==0.10.11 +# homeassistant.components.lg_soundbar +temescal==0.5 + # homeassistant.components.powerwall tesla-powerwall==0.3.18 diff --git a/tests/components/lg_soundbar/__init__.py b/tests/components/lg_soundbar/__init__.py new file mode 100644 index 00000000000..8756d343130 --- /dev/null +++ b/tests/components/lg_soundbar/__init__.py @@ -0,0 +1 @@ +"""Tests for the lg_soundbar component.""" diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py new file mode 100644 index 00000000000..3fafc2c7628 --- /dev/null +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -0,0 +1,95 @@ +"""Test the lg_soundbar config flow.""" +from unittest.mock import MagicMock, patch + +from homeassistant import config_entries +from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", + return_value=MagicMock(), + ), patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + return_value={"uuid": "uuid", "name": "name"}, + ), patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_already_configured(hass): + """Test we handle already configured error.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 0000, + }, + unique_id="uuid", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + return_value={"uuid": "uuid", "name": "name"}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" From 8bcccb17f9119148e456455143064c19b5ea8363 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 30 Jun 2022 13:03:39 -0400 Subject: [PATCH 026/820] Fix ZHA events for logbook (#74245) --- homeassistant/components/zha/logbook.py | 10 +++++++--- tests/components/zha/test_logbook.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index e2d238ddbe8..8140a5244f1 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -62,14 +62,18 @@ def async_describe_events( break if event_type is None: - event_type = event_data[ATTR_COMMAND] + event_type = event_data.get(ATTR_COMMAND, ZHA_EVENT) if event_subtype is not None and event_subtype != event_type: event_type = f"{event_type} - {event_subtype}" - event_type = event_type.replace("_", " ").title() + if event_type is not None: + event_type = event_type.replace("_", " ").title() + if "event" in event_type.lower(): + message = f"{event_type} was fired" + else: + message = f"{event_type} event was fired" - message = f"{event_type} event was fired" if event_data["params"]: message = f"{message} with parameters: {event_data['params']}" diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 33b758fd0a7..6c28284b1e6 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -172,6 +172,19 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): }, }, ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": { + "test": "test", + }, + }, + ), ], ) @@ -182,6 +195,12 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): == "Shake event was fired with parameters: {'test': 'test'}" ) + assert events[1]["name"] == "FakeManufacturer FakeModel" + assert events[1]["domain"] == "zha" + assert ( + events[1]["message"] == "Zha Event was fired with parameters: {'test': 'test'}" + ) + async def test_zha_logbook_event_device_no_device(hass, mock_devices): """Test zha logbook events without device and without triggers.""" From 105b1b9d58cdc72abaf6fa12deeaa73d42b1f208 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 19:04:59 +0200 Subject: [PATCH 027/820] Update numpy to 1.23.0 (#74250) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index eb3135954b0..509d5740b22 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.22.4"], + "requirements": ["numpy==1.23.0"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 8b0dacd3575..7485ff9d608 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.22.4", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.23.0", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 8cd1604f106..0272feb0f9e 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.22.4", "opencv-python-headless==4.6.0.66"], + "requirements": ["numpy==1.23.0", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 4168d820fb6..42d0eae1ecd 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.22.4", + "numpy==1.23.0", "pillow==9.1.1" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 578aea3bbc6..b579cc036bb 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.22.4"], + "requirements": ["numpy==1.23.0"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9ba945c3e2b..6b7b689b93d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -88,7 +88,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy>=1.22.0 +numpy==1.23.0 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index d437757bacd..945fa01b959 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1126,7 +1126,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.4 +numpy==1.23.0 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e29624cd2f4..cc7aedb43c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.4 +numpy==1.23.0 # homeassistant.components.google oauth2client==4.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a2a0eab897a..11e88976e83 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,7 +106,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy>=1.22.0 +numpy==1.23.0 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 From ce03157f16bc60abae6fbb2d8936eca23064183d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Jun 2022 12:05:29 -0500 Subject: [PATCH 028/820] Add debug logging to esphome state updates (#74260) --- homeassistant/components/esphome/entry_data.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 7980d1a6a17..d4bcc67db4a 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable from dataclasses import dataclass, field +import logging from typing import Any, cast from aioesphomeapi import ( @@ -36,6 +37,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store SAVE_DELAY = 120 +_LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { @@ -128,6 +130,12 @@ class RuntimeEntryData: component_key = self.key_to_component[state.key] self.state[component_key][state.key] = state signal = f"esphome_{self.entry_id}_update_{component_key}_{state.key}" + _LOGGER.debug( + "Dispatching update for component %s with state key %s: %s", + component_key, + state.key, + state, + ) async_dispatcher_send(hass, signal) @callback From f311d53c60a11c18ae4dbbcc9d1ab1ebfd3cdbf7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 1 Jul 2022 05:07:03 +1200 Subject: [PATCH 029/820] ESPHome use dhcp responses to update connection host of known devices (#74206) * ESPHome use dhcp responses to update connection host of known devices * Add test for dhcp * Add another test to cover when there are no changes required --- .../components/esphome/config_flow.py | 45 +++++++++++++- .../components/esphome/manifest.json | 1 + homeassistant/generated/dhcp.py | 1 + tests/components/esphome/test_config_flow.py | 60 ++++++++++++++++++- 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 76359cda4e7..ecfa381bc69 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -16,7 +16,7 @@ from aioesphomeapi import ( ) import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback @@ -189,6 +189,49 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle DHCP discovery.""" + node_name = discovery_info.hostname + + await self.async_set_unique_id(node_name) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + + for entry in self._async_current_entries(): + found = False + + if CONF_HOST in entry.data and entry.data[CONF_HOST] in ( + discovery_info.ip, + f"{node_name}.local", + ): + # Is this address or IP address already configured? + found = True + elif DomainData.get(self.hass).is_entry_loaded(entry): + # Does a config entry with this name already exist? + data = DomainData.get(self.hass).get_entry_data(entry) + + # Node names are unique in the network + if data.device_info is not None: + found = data.device_info.name == node_name + + if found: + # Backwards compat, we update old entries + if not entry.unique_id: + self.hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_HOST: discovery_info.ip, + }, + unique_id=node_name, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + break + + return self.async_abort(reason="already_configured") + @callback def _async_get_entry(self) -> FlowResult: config_data = { diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b89671c6f90..a8a76c2b0c8 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": ["aioesphomeapi==10.10.0"], "zeroconf": ["_esphomelib._tcp.local."], + "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], "iot_class": "local_push", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 91398ed00ef..e9cf6ca4c06 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -28,6 +28,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'elkm1', 'macaddress': '00409D*'}, {'domain': 'emonitor', 'hostname': 'emonitor*', 'macaddress': '0090C2*'}, {'domain': 'emonitor', 'registered_devices': True}, + {'domain': 'esphome', 'registered_devices': True}, {'domain': 'flume', 'hostname': 'flume-gw-*'}, {'domain': 'flux_led', 'registered_devices': True}, {'domain': 'flux_led', 'hostname': '[ba][lk]*', 'macaddress': '18B905*'}, diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index f7da5d66bd5..1d2cff051ae 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -12,7 +12,7 @@ from aioesphomeapi import ( import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import ( @@ -532,3 +532,61 @@ async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" + + +async def test_discovery_dhcp_updates_host(hass, mock_client): + """Test dhcp discovery updates host and aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + entry.add_to_hass(hass) + + mock_entry_data = MagicMock() + mock_entry_data.device_info.name = "test8266" + domain_data = DomainData.get(hass) + domain_data.set_entry_data(entry, mock_entry_data) + + service_info = dhcp.DhcpServiceInfo( + ip="192.168.43.184", + hostname="test8266", + macaddress="00:00:00:00:00:00", + ) + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.184" + + +async def test_discovery_dhcp_no_changes(hass, mock_client): + """Test dhcp discovery updates host and aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + entry.add_to_hass(hass) + + mock_entry_data = MagicMock() + mock_entry_data.device_info.name = "test8266" + domain_data = DomainData.get(hass) + domain_data.set_entry_data(entry, mock_entry_data) + + service_info = dhcp.DhcpServiceInfo( + ip="192.168.43.183", + hostname="test8266", + macaddress="00:00:00:00:00:00", + ) + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.183" From 1bdd93cc776ac95095f42666e8c0ef5b3c5ca9ae Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 30 Jun 2022 12:10:05 -0500 Subject: [PATCH 030/820] Fix Life360 unload (#74263) * Fix life360 async_unload_entry * Update tracked_members when unloading config entry --- homeassistant/components/life360/__init__.py | 14 ++++++++++---- homeassistant/components/life360/device_tracker.py | 10 +++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index 66c9416a1c1..4527f6ac298 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -124,11 +124,11 @@ class IntegData: """Integration data.""" cfg_options: dict[str, Any] | None = None - # ConfigEntry.unique_id: Life360DataUpdateCoordinator + # ConfigEntry.entry_id: Life360DataUpdateCoordinator coordinators: dict[str, Life360DataUpdateCoordinator] = field( init=False, default_factory=dict ) - # member_id: ConfigEntry.unique_id + # member_id: ConfigEntry.entry_id tracked_members: dict[str, str] = field(init=False, default_factory=dict) logged_circles: list[str] = field(init=False, default_factory=list) logged_places: list[str] = field(init=False, default_factory=list) @@ -171,7 +171,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload config entry.""" - del hass.data[DOMAIN].coordinators[entry.entry_id] # Unload components for our platforms. - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN].coordinators[entry.entry_id] + # Remove any members that were tracked by this entry. + for member_id, entry_id in hass.data[DOMAIN].tracked_members.copy().items(): + if entry_id == entry.entry_id: + del hass.data[DOMAIN].tracked_members[member_id] + + return unload_ok diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index a38181a6830..5a18422487e 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -78,13 +78,13 @@ async def async_setup_entry( new_entities = [] for member_id, member in coordinator.data.members.items(): - tracked_by_account = tracked_members.get(member_id) - if new_member := not tracked_by_account: - tracked_members[member_id] = entry.unique_id - LOGGER.debug("Member: %s", member.name) + tracked_by_entry = tracked_members.get(member_id) + if new_member := not tracked_by_entry: + tracked_members[member_id] = entry.entry_id + LOGGER.debug("Member: %s (%s)", member.name, entry.unique_id) if ( new_member - or tracked_by_account == entry.unique_id + or tracked_by_entry == entry.entry_id and not new_members_only ): new_entities.append(Life360DeviceTracker(coordinator, member_id)) From f05b4a0ca05c5090ce007bbd0e22f2e76c643270 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 Jun 2022 19:15:25 +0200 Subject: [PATCH 031/820] Fire event_mqtt_reloaded only after reload is completed (#74226) --- homeassistant/components/mqtt/__init__.py | 70 +++++++++++-------- .../components/mqtt/alarm_control_panel.py | 6 +- .../components/mqtt/binary_sensor.py | 6 +- homeassistant/components/mqtt/button.py | 6 +- homeassistant/components/mqtt/camera.py | 6 +- homeassistant/components/mqtt/climate.py | 6 +- homeassistant/components/mqtt/const.py | 3 +- homeassistant/components/mqtt/cover.py | 6 +- homeassistant/components/mqtt/fan.py | 4 +- homeassistant/components/mqtt/humidifier.py | 6 +- .../components/mqtt/light/__init__.py | 6 +- homeassistant/components/mqtt/lock.py | 6 +- homeassistant/components/mqtt/mixins.py | 46 +++++------- homeassistant/components/mqtt/number.py | 6 +- homeassistant/components/mqtt/scene.py | 6 +- homeassistant/components/mqtt/select.py | 6 +- homeassistant/components/mqtt/sensor.py | 6 +- homeassistant/components/mqtt/siren.py | 6 +- homeassistant/components/mqtt/switch.py | 6 +- .../components/mqtt/vacuum/__init__.py | 6 +- 20 files changed, 96 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 6fd288a86cf..a099e7b580c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,14 +28,12 @@ from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import ( async_integration_yaml_config, - async_setup_reload_service, + async_reload_integration_platforms, ) +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow @@ -78,10 +76,10 @@ from .const import ( # noqa: F401 DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - MQTT_RELOADED, PLATFORMS, RELOADABLE_PLATFORMS, ) +from .mixins import async_discover_yaml_entities from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -241,7 +239,9 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) @@ -378,16 +378,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() - # Setup reload service. Once support for legacy config is removed in 2022.9, we - # should no longer call async_setup_reload_service but instead implement a custom - # service - await async_setup_reload_service(hass, DOMAIN, RELOADABLE_PLATFORMS) + async def async_setup_reload_service() -> None: + """Create the reload service for the MQTT domain.""" + if hass.services.has_service(DOMAIN, SERVICE_RELOAD): + return - async def _async_reload_platforms(_: Event | None) -> None: - """Discover entities for a platform.""" - config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} - hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) - async_dispatcher_send(hass, MQTT_RELOADED) + async def _reload_config(call: ServiceCall) -> None: + """Reload the platforms.""" + # Reload the legacy yaml platform + await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + + # Reload the modern yaml platforms + config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} + hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) + await asyncio.gather( + *( + [ + async_discover_yaml_entities(hass, component) + for component in RELOADABLE_PLATFORMS + ] + ) + ) + + # Fire event + hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) + + async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config) async def async_forward_entry_setup_and_setup_discovery(config_entry): """Forward the config entry setup to the platforms and set up discovery.""" @@ -411,21 +427,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if conf.get(CONF_DISCOVERY): await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded - entry.async_on_unload( - hass.bus.async_listen("event_mqtt_reloaded", _async_reload_platforms) - ) + await async_setup_reload_service() + + if DATA_MQTT_RELOAD_NEEDED in hass.data: + hass.data.pop(DATA_MQTT_RELOAD_NEEDED) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=False, + ) hass.async_create_task(async_forward_entry_setup_and_setup_discovery(entry)) - if DATA_MQTT_RELOAD_NEEDED in hass.data: - hass.data.pop(DATA_MQTT_RELOAD_NEEDED) - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=False, - ) - return True diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 8e5ee54d688..b6f2f8f236e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -44,8 +44,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -147,9 +147,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, alarm.DOMAIN) - ) + await async_discover_yaml_entities(hass, alarm.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 39fd87c8b02..9e0a049b15e 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -41,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -102,9 +102,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, binary_sensor.DOMAIN) - ) + await async_discover_yaml_entities(hass, binary_sensor.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 0374727bf7d..b75fbe4b97f 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -25,8 +25,8 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -82,9 +82,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, button.DOMAIN) - ) + await async_discover_yaml_entities(hass, button.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 5c8d3bc48b2..69af7992229 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -22,8 +22,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -80,9 +80,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, camera.DOMAIN) - ) + await async_discover_yaml_entities(hass, camera.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index a26e9cba8df..6b09891483c 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -50,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -391,9 +391,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, climate.DOMAIN) - ) + await async_discover_yaml_entities(hass, climate.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 67a9208faba..6ac77021337 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -64,7 +64,6 @@ DOMAIN = "mqtt" MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" -MQTT_RELOADED = "mqtt_reloaded" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" @@ -105,8 +104,8 @@ RELOADABLE_PLATFORMS = [ Platform.LIGHT, Platform.LOCK, Platform.NUMBER, - Platform.SELECT, Platform.SCENE, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 54ed4f2b0a0..14746329250 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -46,8 +46,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -242,9 +242,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, cover.DOMAIN) - ) + await async_discover_yaml_entities(hass, cover.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 721fa93f244..15e4a80f3e7 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -50,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -232,7 +232,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload(await async_setup_platform_discovery(hass, fan.DOMAIN)) + await async_discover_yaml_entities(hass, fan.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index d2856767cf0..5f09fc0d513 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -45,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -187,9 +187,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, humidifier.DOMAIN) - ) + await async_discover_yaml_entities(hass, humidifier.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index d4914cb9506..c7f3395ba4e 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -13,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -111,9 +111,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, light.DOMAIN) - ) + await async_discover_yaml_entities(hass, light.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 1d6a40c2331..b4788f1db0c 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -28,8 +28,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -103,9 +103,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, lock.DOMAIN) - ) + await async_discover_yaml_entities(hass, lock.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 10fe6cb6cc5..8e59d09dfce 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -26,7 +26,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -69,7 +69,6 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - MQTT_RELOADED, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -261,34 +260,27 @@ class SetupEntity(Protocol): """Define setup_entities type.""" -async def async_setup_platform_discovery( +async def async_discover_yaml_entities( hass: HomeAssistant, platform_domain: str -) -> CALLBACK_TYPE: - """Set up platform discovery for manual config.""" - - async def _async_discover_entities() -> None: - """Discover entities for a platform.""" - if DATA_MQTT_UPDATED_CONFIG in hass.data: - # The platform has been reloaded - config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] - else: - config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) - if not config_yaml: - return - if platform_domain not in config_yaml: - return - await asyncio.gather( - *( - discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) - for config in await async_get_platform_config_from_yaml( - hass, platform_domain, config_yaml - ) +) -> None: + """Discover entities for a platform.""" + if DATA_MQTT_UPDATED_CONFIG in hass.data: + # The platform has been reloaded + config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] + else: + config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) + if not config_yaml: + return + if platform_domain not in config_yaml: + return + await asyncio.gather( + *( + discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) + for config in await async_get_platform_config_from_yaml( + hass, platform_domain, config_yaml ) ) - - unsub = async_dispatcher_connect(hass, MQTT_RELOADED, _async_discover_entities) - await _async_discover_entities() - return unsub + ) async def async_get_platform_config_from_yaml( diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 660ffe987f0..dc27a740720 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -41,8 +41,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -135,9 +135,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, number.DOMAIN) - ) + await async_discover_yaml_entities(hass, number.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index cc911cc3431..8b654f7cca0 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -22,8 +22,8 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -79,9 +79,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, scene.DOMAIN) - ) + await async_discover_yaml_entities(hass, scene.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 0d9f1411fd1..4c302446b19 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -30,8 +30,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -94,9 +94,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, select.DOMAIN) - ) + await async_discover_yaml_entities(hass, select.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 672e22f632f..6948e173039 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -41,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -147,9 +147,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, sensor.DOMAIN) - ) + await async_discover_yaml_entities(hass, sensor.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index a6cff4cf91d..dfb89d2ee79 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -51,8 +51,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -143,9 +143,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, siren.DOMAIN) - ) + await async_discover_yaml_entities(hass, siren.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index dadd5f86f20..b04f2433659 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -37,8 +37,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -97,9 +97,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, switch.DOMAIN) - ) + await async_discover_yaml_entities(hass, switch.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 694e9530939..c49b8cfa012 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -12,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, ) from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE @@ -91,9 +91,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, vacuum.DOMAIN) - ) + await async_discover_yaml_entities(hass, vacuum.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry From 2723ca0b850536011d79498e6c21f16c88d66269 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Jun 2022 12:39:36 -0500 Subject: [PATCH 032/820] Filter out CONF_SCAN_INTERVAL from scrape import (#74254) --- homeassistant/components/scrape/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 1c447439820..a73dbc17c1c 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, + CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, @@ -90,7 +91,7 @@ async def async_setup_platform( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data=config, + data={k: v for k, v in config.items() if k != CONF_SCAN_INTERVAL}, ) ) From dc22726425107d5fdb8595a4d80ecd238f18b0cc Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Thu, 30 Jun 2022 19:45:11 +0200 Subject: [PATCH 033/820] Optimize optionflow tests (#74262) * Optimize optionflow tests * Extend mocking --- tests/components/nina/test_config_flow.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index 1578991ba11..738eb80e190 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -163,6 +163,9 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -213,6 +216,8 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -248,7 +253,12 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: with patch( "pynina.baseApi.BaseAPI._makeRequest", side_effect=ApiError("Could not connect to Api"), + ), patch( + "homeassistant.components.nina.async_setup_entry", + return_value=True, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -268,7 +278,12 @@ async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: with patch( "pynina.baseApi.BaseAPI._makeRequest", side_effect=Exception("DUMMY"), + ), patch( + "homeassistant.components.nina.async_setup_entry", + return_value=True, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) From 25a5ebe0c77c53b489c50a6c70187184a9fd243d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 30 Jun 2022 19:47:29 +0200 Subject: [PATCH 034/820] Met.no use native_* (#74259) --- homeassistant/components/met/const.py | 16 +++---- homeassistant/components/met/weather.py | 57 ++++++------------------- 2 files changed, 20 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index 93f9e3414dd..5b2a756847e 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -11,13 +11,13 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -173,13 +173,13 @@ CONDITIONS_MAP = { FORECAST_MAP = { ATTR_FORECAST_CONDITION: "condition", - ATTR_FORECAST_PRECIPITATION: "precipitation", + ATTR_FORECAST_NATIVE_PRECIPITATION: "precipitation", ATTR_FORECAST_PRECIPITATION_PROBABILITY: "precipitation_probability", - ATTR_FORECAST_TEMP: "temperature", - ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_NATIVE_TEMP: "temperature", + ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_TIME: "datetime", ATTR_FORECAST_WIND_BEARING: "wind_bearing", - ATTR_FORECAST_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", } ATTR_MAP = { diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 251d99ad295..0ff0a60bfa1 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -6,7 +6,6 @@ from typing import Any from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, @@ -21,12 +20,9 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_INCHES, LENGTH_MILLIMETERS, PRESSURE_HPA, - PRESSURE_INHG, SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -34,19 +30,9 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed from . import MetDataUpdateCoordinator -from .const import ( - ATTR_FORECAST_PRECIPITATION, - ATTR_MAP, - CONDITIONS_MAP, - CONF_TRACK_HOME, - DOMAIN, - FORECAST_MAP, -) +from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP ATTRIBUTION = ( "Weather forecast from met.no, delivered by the Norwegian " @@ -85,6 +71,11 @@ def format_condition(condition: str) -> str: class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__( self, coordinator: MetDataUpdateCoordinator, @@ -144,27 +135,18 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): return format_condition(condition) @property - def temperature(self) -> float | None: + def native_temperature(self) -> float | None: """Return the temperature.""" return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_TEMPERATURE] ) @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self) -> float | None: + def native_pressure(self) -> float | None: """Return the pressure.""" - pressure_hpa = self.coordinator.data.current_weather_data.get( + return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_PRESSURE] ) - if self._is_metric or pressure_hpa is None: - return pressure_hpa - - return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) @property def humidity(self) -> float | None: @@ -174,18 +156,11 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): ) @property - def wind_speed(self) -> float | None: + def native_wind_speed(self) -> float | None: """Return the wind speed.""" - speed_km_h = self.coordinator.data.current_weather_data.get( + return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_WIND_SPEED] ) - if self._is_metric or speed_km_h is None: - return speed_km_h - - speed_mi_h = convert_speed( - speed_km_h, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR - ) - return int(round(speed_mi_h)) @property def wind_bearing(self) -> float | str | None: @@ -206,7 +181,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): met_forecast = self.coordinator.data.hourly_forecast else: met_forecast = self.coordinator.data.daily_forecast - required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + required_keys = {"temperature", ATTR_FORECAST_TIME} ha_forecast: list[Forecast] = [] for met_item in met_forecast: if not set(met_item).issuperset(required_keys): @@ -216,14 +191,6 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): for k, v in FORECAST_MAP.items() if met_item.get(v) is not None } - if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: - if ha_item[ATTR_FORECAST_PRECIPITATION] is not None: - precip_inches = convert_distance( - ha_item[ATTR_FORECAST_PRECIPITATION], - LENGTH_MILLIMETERS, - LENGTH_INCHES, - ) - ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From 382b5d5073362b96976ce94f718f7fa4db6b628a Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 30 Jun 2022 13:01:23 -0500 Subject: [PATCH 035/820] Bump frontend to 20220630.0 (#74266) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 23f056ba0da..27ff0a73f20 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220629.0"], + "requirements": ["home-assistant-frontend==20220630.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6b7b689b93d..7ee5a9fe8d3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 945fa01b959..eac0800e73a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc7aedb43c8..f4b5e587f7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 # homeassistant.components.home_connect homeconnect==0.7.1 From e3d250a6234d0f19673d07d4e274a80d0b1d7e12 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 20:12:58 +0200 Subject: [PATCH 036/820] Do not pin numpy in wheels (#74268) --- .github/workflows/wheels.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 95f1d8e437e..2ffc2f1f721 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -157,6 +157,9 @@ jobs: echo "cmake==3.22.2" ) >> homeassistant/package_constraints.txt + # Do not pin numpy in wheels building + sed -i "/numpy/d" homeassistant/package_constraints.txt + - name: Build wheels uses: home-assistant/wheels@2022.06.7 with: From 3899c9e6d34a8254b03bc11f1f69f1a0c5478739 Mon Sep 17 00:00:00 2001 From: "R. de Veen" Date: Thu, 30 Jun 2022 20:27:31 +0200 Subject: [PATCH 037/820] Links to Esphomelib.com is changed to esphome.io (#72680) --- homeassistant/components/esphome/config_flow.py | 6 +++++- homeassistant/components/esphome/strings.json | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index ecfa381bc69..9fd12634e43 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -25,6 +25,7 @@ from homeassistant.data_entry_flow import FlowResult from . import CONF_NOISE_PSK, DOMAIN, DomainData ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" +ESPHOME_URL = "https://esphome.io/" class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): @@ -55,7 +56,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = error return self.async_show_form( - step_id="user", data_schema=vol.Schema(fields), errors=errors + step_id="user", + data_schema=vol.Schema(fields), + errors=errors, + description_placeholders={"esphome_url": ESPHOME_URL}, ) async def async_step_user( diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 62814f2723b..b1b1ba94e3f 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -6,7 +6,7 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips", + "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address", "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_psk": "The transport encryption key is invalid. Please ensure it matches what you have in your configuration" @@ -17,7 +17,7 @@ "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" }, - "description": "Please enter connection settings of your [ESPHome](https://esphomelib.com/) node." + "description": "Please enter connection settings of your [ESPHome]({esphome_url}) node." }, "authenticate": { "data": { From 1bfd8b1a7610963894c58e0291df257656d467b3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 20:49:41 +0200 Subject: [PATCH 038/820] Add enforce_type_hints to vscode tasks (#74227) --- .github/workflows/ci.yaml | 4 ++-- .pre-commit-config.yaml | 2 +- pylint/plugins/hass_enforce_type_hints.py | 2 +- tests/pylint/test_enforce_type_hints.py | 6 ++++++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54a1997b28e..1d7a5c95986 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -648,14 +648,14 @@ jobs: run: | . venv/bin/activate python --version - pylint homeassistant + pylint --ignore-missing-annotations=y homeassistant - name: Run pylint (partially) if: needs.changes.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate python --version - pylint homeassistant/components/${{ needs.changes.outputs.integrations_glob }} + pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.changes.outputs.integrations_glob }} mypy: name: Check mypy diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6400e113c5..5da3a6b21ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -96,7 +96,7 @@ repos: files: ^(homeassistant|pylint)/.+\.py$ - id: pylint name: pylint - entry: script/run-in-env.sh pylint -j 0 + entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y language: script types: [python] files: ^homeassistant/.+\.py$ diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 3ac072373b8..2696c9965e4 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1268,7 +1268,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ( "ignore-missing-annotations", { - "default": True, + "default": False, "type": "yn", "metavar": "", "help": "Set to ``no`` if you wish to check functions that do not " diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 8b4b8d4d058..df10c7514c6 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -120,6 +120,9 @@ def test_ignore_no_annotations( hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str ) -> None: """Ensure that _is_valid_type is not run if there are no annotations.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = True + func_node = astroid.extract_node( code, "homeassistant.components.pylint_test", @@ -539,6 +542,9 @@ def test_ignore_invalid_entity_properties( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Check invalid entity properties are ignored by default.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = True + class_node = astroid.extract_node( """ class LockEntity(): From 0caeeb56c5efd5b07d110a44c297cbda85a43c60 Mon Sep 17 00:00:00 2001 From: "Erik J. Olson" Date: Thu, 30 Jun 2022 14:00:29 -0500 Subject: [PATCH 039/820] Add Matrix.io HTML message format support (#69951) --- homeassistant/components/matrix/__init__.py | 18 ++++++++++++++---- homeassistant/components/matrix/const.py | 3 +++ homeassistant/components/matrix/services.yaml | 4 ++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 47646a586ca..17df46a0849 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.util.json import load_json, save_json -from .const import DOMAIN, SERVICE_SEND_MESSAGE +from .const import DOMAIN, FORMAT_HTML, FORMAT_TEXT, SERVICE_SEND_MESSAGE _LOGGER = logging.getLogger(__name__) @@ -36,8 +36,12 @@ CONF_EXPRESSION = "expression" DEFAULT_CONTENT_TYPE = "application/octet-stream" +MESSAGE_FORMATS = [FORMAT_HTML, FORMAT_TEXT] +DEFAULT_MESSAGE_FORMAT = FORMAT_TEXT + EVENT_MATRIX_COMMAND = "matrix_command" +ATTR_FORMAT = "format" # optional message format ATTR_IMAGES = "images" # optional images COMMAND_SCHEMA = vol.All( @@ -74,7 +78,10 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema( { vol.Required(ATTR_MESSAGE): cv.string, - vol.Optional(ATTR_DATA): { + vol.Optional(ATTR_DATA, default={}): { + vol.Optional(ATTR_FORMAT, default=DEFAULT_MESSAGE_FORMAT): vol.In( + MESSAGE_FORMATS + ), vol.Optional(ATTR_IMAGES): vol.All(cv.ensure_list, [cv.string]), }, vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), @@ -377,7 +384,10 @@ class MatrixBot: try: room = self._join_or_get_room(target_room) if message is not None: - _LOGGER.debug(room.send_text(message)) + if data.get(ATTR_FORMAT) == FORMAT_HTML: + _LOGGER.debug(room.send_html(message)) + else: + _LOGGER.debug(room.send_text(message)) except MatrixRequestError as ex: _LOGGER.error( "Unable to deliver message to room '%s': %d, %s", @@ -385,7 +395,7 @@ class MatrixBot: ex.code, ex.content, ) - if data is not None: + if ATTR_IMAGES in data: for img in data.get(ATTR_IMAGES, []): self._send_image(img, target_rooms) diff --git a/homeassistant/components/matrix/const.py b/homeassistant/components/matrix/const.py index 6b082bde121..b7e0c22e2ac 100644 --- a/homeassistant/components/matrix/const.py +++ b/homeassistant/components/matrix/const.py @@ -2,3 +2,6 @@ DOMAIN = "matrix" SERVICE_SEND_MESSAGE = "send_message" + +FORMAT_HTML = "html" +FORMAT_TEXT = "text" diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index c58a27c3370..9b5171d1483 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -18,7 +18,7 @@ send_message: text: data: name: Data - description: Extended information of notification. Supports list of images. Optional. - example: "{'images': ['/tmp/test.jpg']}" + description: Extended information of notification. Supports list of images. Supports message format. Optional. + example: "{'images': ['/tmp/test.jpg'], 'format': 'text'}" selector: object: From 768b98ae77329e98b2993c2e4fa7435ad6f1b49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 30 Jun 2022 21:09:08 +0200 Subject: [PATCH 040/820] Add QNAP QSW Update platform (#71019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * qnap_qsw: add Update platform Signed-off-by: Álvaro Fernández Rojas * qnap_qsw: update: allow init if firmware coordinator fails QSW API can return an error if update servers aren't reachable and this prevents the integration from loading. Signed-off-by: Álvaro Fernández Rojas * tests: qnap_qsw: achieve 100% coverage Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/__init__.py | 23 +++-- .../components/qnap_qsw/binary_sensor.py | 8 +- homeassistant/components/qnap_qsw/button.py | 12 +-- homeassistant/components/qnap_qsw/const.py | 3 + .../components/qnap_qsw/coordinator.py | 35 +++++++- .../components/qnap_qsw/diagnostics.py | 11 ++- homeassistant/components/qnap_qsw/entity.py | 43 ++++++++- homeassistant/components/qnap_qsw/sensor.py | 8 +- homeassistant/components/qnap_qsw/update.py | 89 +++++++++++++++++++ tests/components/qnap_qsw/test_coordinator.py | 35 ++++++-- tests/components/qnap_qsw/test_init.py | 3 + tests/components/qnap_qsw/test_update.py | 26 ++++++ tests/components/qnap_qsw/util.py | 27 ++++++ 13 files changed, 286 insertions(+), 37 deletions(-) create mode 100644 homeassistant/components/qnap_qsw/update.py create mode 100644 tests/components/qnap_qsw/test_update.py diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index 26ed8066686..4d2ee76cd2b 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -8,10 +8,15 @@ from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .const import DOMAIN -from .coordinator import QswUpdateCoordinator +from .const import DOMAIN, QSW_COORD_DATA, QSW_COORD_FW +from .coordinator import QswDataCoordinator, QswFirmwareCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.SENSOR, + Platform.UPDATE, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -24,10 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: qsw = QnapQswApi(aiohttp_client.async_get_clientsession(hass), options) - coordinator = QswUpdateCoordinator(hass, qsw) - await coordinator.async_config_entry_first_refresh() + coord_data = QswDataCoordinator(hass, qsw) + await coord_data.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + coord_fw = QswFirmwareCoordinator(hass, qsw) + await coord_fw.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + QSW_COORD_DATA: coord_data, + QSW_COORD_FW: coord_fw, + } hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/qnap_qsw/binary_sensor.py b/homeassistant/components/qnap_qsw/binary_sensor.py index 467a3314070..71af89778b8 100644 --- a/homeassistant/components/qnap_qsw/binary_sensor.py +++ b/homeassistant/components/qnap_qsw/binary_sensor.py @@ -16,8 +16,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_MESSAGE, DOMAIN -from .coordinator import QswUpdateCoordinator +from .const import ATTR_MESSAGE, DOMAIN, QSW_COORD_DATA +from .coordinator import QswDataCoordinator from .entity import QswEntityDescription, QswSensorEntity @@ -48,7 +48,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW binary sensors from a config_entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] async_add_entities( QswBinarySensor(coordinator, description, entry) for description in BINARY_SENSOR_TYPES @@ -66,7 +66,7 @@ class QswBinarySensor(QswSensorEntity, BinarySensorEntity): def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, description: QswBinarySensorEntityDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/qnap_qsw/button.py b/homeassistant/components/qnap_qsw/button.py index 1c13310fe05..9aad411b992 100644 --- a/homeassistant/components/qnap_qsw/button.py +++ b/homeassistant/components/qnap_qsw/button.py @@ -17,9 +17,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, QSW_REBOOT -from .coordinator import QswUpdateCoordinator -from .entity import QswEntity +from .const import DOMAIN, QSW_COORD_DATA, QSW_REBOOT +from .coordinator import QswDataCoordinator +from .entity import QswDataEntity @dataclass @@ -49,20 +49,20 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW buttons from a config_entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] async_add_entities( QswButton(coordinator, description, entry) for description in BUTTON_TYPES ) -class QswButton(QswEntity, ButtonEntity): +class QswButton(QswDataEntity, ButtonEntity): """Define a QNAP QSW button.""" entity_description: QswButtonDescription def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, description: QswButtonDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/qnap_qsw/const.py b/homeassistant/components/qnap_qsw/const.py index e583c0250f4..4b5fa9a4a2c 100644 --- a/homeassistant/components/qnap_qsw/const.py +++ b/homeassistant/components/qnap_qsw/const.py @@ -10,5 +10,8 @@ MANUFACTURER: Final = "QNAP" RPM: Final = "rpm" +QSW_COORD_DATA: Final = "coordinator-data" +QSW_COORD_FW: Final = "coordinator-firmware" QSW_REBOOT = "reboot" QSW_TIMEOUT_SEC: Final = 25 +QSW_UPDATE: Final = "update" diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index c018c1f3848..73af2f74cc5 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -5,7 +5,7 @@ from datetime import timedelta import logging from typing import Any -from aioqsw.exceptions import QswError +from aioqsw.exceptions import APIError, QswError from aioqsw.localapi import QnapQswApi import async_timeout @@ -14,12 +14,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, QSW_TIMEOUT_SEC -SCAN_INTERVAL = timedelta(seconds=60) +DATA_SCAN_INTERVAL = timedelta(seconds=60) +FW_SCAN_INTERVAL = timedelta(hours=12) _LOGGER = logging.getLogger(__name__) -class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): +class QswDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the QNAP QSW device.""" def __init__(self, hass: HomeAssistant, qsw: QnapQswApi) -> None: @@ -30,7 +31,7 @@ class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): hass, _LOGGER, name=DOMAIN, - update_interval=SCAN_INTERVAL, + update_interval=DATA_SCAN_INTERVAL, ) async def _async_update_data(self) -> dict[str, Any]: @@ -41,3 +42,29 @@ class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except QswError as error: raise UpdateFailed(error) from error return self.qsw.data() + + +class QswFirmwareCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching firmware data from the QNAP QSW device.""" + + def __init__(self, hass: HomeAssistant, qsw: QnapQswApi) -> None: + """Initialize.""" + self.qsw = qsw + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=FW_SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update firmware data via library.""" + async with async_timeout.timeout(QSW_TIMEOUT_SEC): + try: + await self.qsw.check_firmware() + except APIError as error: + _LOGGER.warning(error) + except QswError as error: + raise UpdateFailed(error) from error + return self.qsw.data() diff --git a/homeassistant/components/qnap_qsw/diagnostics.py b/homeassistant/components/qnap_qsw/diagnostics.py index 3730bab24a8..2467e9181a3 100644 --- a/homeassistant/components/qnap_qsw/diagnostics.py +++ b/homeassistant/components/qnap_qsw/diagnostics.py @@ -10,8 +10,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import QswUpdateCoordinator +from .const import DOMAIN, QSW_COORD_DATA, QSW_COORD_FW +from .coordinator import QswDataCoordinator, QswFirmwareCoordinator TO_REDACT_CONFIG = [ CONF_USERNAME, @@ -29,9 +29,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + entry_data = hass.data[DOMAIN][config_entry.entry_id] + coord_data: QswDataCoordinator = entry_data[QSW_COORD_DATA] + coord_fw: QswFirmwareCoordinator = entry_data[QSW_COORD_FW] return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT_CONFIG), - "coord_data": async_redact_data(coordinator.data, TO_REDACT_DATA), + "coord_data": async_redact_data(coord_data.data, TO_REDACT_DATA), + "coord_fw": async_redact_data(coord_fw.data, TO_REDACT_DATA), } diff --git a/homeassistant/components/qnap_qsw/entity.py b/homeassistant/components/qnap_qsw/entity.py index c3550610d83..7da47f9734f 100644 --- a/homeassistant/components/qnap_qsw/entity.py +++ b/homeassistant/components/qnap_qsw/entity.py @@ -20,15 +20,15 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import MANUFACTURER -from .coordinator import QswUpdateCoordinator +from .coordinator import QswDataCoordinator, QswFirmwareCoordinator -class QswEntity(CoordinatorEntity[QswUpdateCoordinator]): +class QswDataEntity(CoordinatorEntity[QswDataCoordinator]): """Define an QNAP QSW entity.""" def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, entry: ConfigEntry, ) -> None: """Initialize.""" @@ -72,7 +72,7 @@ class QswEntityDescription(EntityDescription, QswEntityDescriptionMixin): attributes: dict[str, list[str]] | None = None -class QswSensorEntity(QswEntity): +class QswSensorEntity(QswDataEntity): """Base class for QSW sensor entities.""" entity_description: QswEntityDescription @@ -91,3 +91,38 @@ class QswSensorEntity(QswEntity): key: self.get_device_value(val[0], val[1]) for key, val in self.entity_description.attributes.items() } + + +class QswFirmwareEntity(CoordinatorEntity[QswFirmwareCoordinator]): + """Define a QNAP QSW firmware entity.""" + + def __init__( + self, + coordinator: QswFirmwareCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_device_info = DeviceInfo( + configuration_url=entry.data[CONF_URL], + connections={ + ( + CONNECTION_NETWORK_MAC, + self.get_device_value(QSD_SYSTEM_BOARD, QSD_MAC), + ) + }, + manufacturer=MANUFACTURER, + model=self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT), + name=self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT), + sw_version=self.get_device_value(QSD_FIRMWARE_INFO, QSD_FIRMWARE), + ) + + def get_device_value(self, key: str, subkey: str) -> Any: + """Return device value by key.""" + value = None + if key in self.coordinator.data: + data = self.coordinator.data[key] + if subkey in data: + value = data[subkey] + return value diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index 0de8ec4a39e..618c20b4cc2 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -26,8 +26,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_MAX, DOMAIN, RPM -from .coordinator import QswUpdateCoordinator +from .const import ATTR_MAX, DOMAIN, QSW_COORD_DATA, RPM +from .coordinator import QswDataCoordinator from .entity import QswEntityDescription, QswSensorEntity @@ -82,7 +82,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW sensors from a config_entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] async_add_entities( QswSensor(coordinator, description, entry) for description in SENSOR_TYPES @@ -100,7 +100,7 @@ class QswSensor(QswSensorEntity, SensorEntity): def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, description: QswSensorEntityDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/qnap_qsw/update.py b/homeassistant/components/qnap_qsw/update.py new file mode 100644 index 00000000000..8dfd985ffef --- /dev/null +++ b/homeassistant/components/qnap_qsw/update.py @@ -0,0 +1,89 @@ +"""Support for the QNAP QSW update.""" +from __future__ import annotations + +from typing import Final + +from aioqsw.const import ( + QSD_DESCRIPTION, + QSD_FIRMWARE_CHECK, + QSD_FIRMWARE_INFO, + QSD_PRODUCT, + QSD_SYSTEM_BOARD, + QSD_VERSION, +) + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, QSW_COORD_FW, QSW_UPDATE +from .coordinator import QswFirmwareCoordinator +from .entity import QswFirmwareEntity + +UPDATE_TYPES: Final[tuple[UpdateEntityDescription, ...]] = ( + UpdateEntityDescription( + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + key=QSW_UPDATE, + name="Firmware Update", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add QNAP QSW updates from a config_entry.""" + coordinator: QswFirmwareCoordinator = hass.data[DOMAIN][entry.entry_id][ + QSW_COORD_FW + ] + async_add_entities( + QswUpdate(coordinator, description, entry) for description in UPDATE_TYPES + ) + + +class QswUpdate(QswFirmwareEntity, UpdateEntity): + """Define a QNAP QSW update.""" + + entity_description: UpdateEntityDescription + + def __init__( + self, + coordinator: QswFirmwareCoordinator, + description: UpdateEntityDescription, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry) + self._attr_name = ( + f"{self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT)} {description.name}" + ) + self._attr_unique_id = f"{entry.unique_id}_{description.key}" + self.entity_description = description + + self._attr_installed_version = self.get_device_value( + QSD_FIRMWARE_INFO, QSD_VERSION + ) + self._async_update_attrs() + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update attributes.""" + self._attr_latest_version = self.get_device_value( + QSD_FIRMWARE_CHECK, QSD_VERSION + ) + self._attr_release_summary = self.get_device_value( + QSD_FIRMWARE_CHECK, QSD_DESCRIPTION + ) diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index b8e4855fea9..107cfa580b7 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -2,10 +2,13 @@ from unittest.mock import patch -from aioqsw.exceptions import QswError +from aioqsw.exceptions import APIError, QswError from homeassistant.components.qnap_qsw.const import DOMAIN -from homeassistant.components.qnap_qsw.coordinator import SCAN_INTERVAL +from homeassistant.components.qnap_qsw.coordinator import ( + DATA_SCAN_INTERVAL, + FW_SCAN_INTERVAL, +) from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow @@ -14,6 +17,7 @@ from .util import ( CONFIG, FIRMWARE_CONDITION_MOCK, FIRMWARE_INFO_MOCK, + FIRMWARE_UPDATE_CHECK_MOCK, SYSTEM_BOARD_MOCK, SYSTEM_SENSOR_MOCK, SYSTEM_TIME_MOCK, @@ -37,6 +41,9 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_info", return_value=FIRMWARE_INFO_MOCK, ) as mock_firmware_info, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", + return_value=FIRMWARE_UPDATE_CHECK_MOCK, + ) as mock_firmware_update_check, patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, ) as mock_system_board, patch( @@ -57,14 +64,16 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.assert_called_once() mock_firmware_info.assert_called_once() + mock_firmware_update_check.assert_called_once() mock_system_board.assert_called_once() mock_system_sensor.assert_called_once() mock_system_time.assert_called_once() - mock_users_verification.assert_not_called() + mock_users_verification.assert_called_once() mock_users_login.assert_called_once() mock_firmware_condition.reset_mock() mock_firmware_info.reset_mock() + mock_firmware_update_check.reset_mock() mock_system_board.reset_mock() mock_system_sensor.reset_mock() mock_system_time.reset_mock() @@ -72,12 +81,28 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_users_login.reset_mock() mock_system_sensor.side_effect = QswError - async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + async_fire_time_changed(hass, utcnow() + DATA_SCAN_INTERVAL) await hass.async_block_till_done() mock_system_sensor.assert_called_once() - mock_users_verification.assert_called_once() + mock_users_verification.assert_called() mock_users_login.assert_not_called() state = hass.states.get("sensor.qsw_m408_4c_temperature") assert state.state == STATE_UNAVAILABLE + + mock_firmware_update_check.side_effect = APIError + async_fire_time_changed(hass, utcnow() + FW_SCAN_INTERVAL) + await hass.async_block_till_done() + + mock_firmware_update_check.assert_called_once() + mock_firmware_update_check.reset_mock() + + mock_firmware_update_check.side_effect = QswError + async_fire_time_changed(hass, utcnow() + FW_SCAN_INTERVAL) + await hass.async_block_till_done() + + mock_firmware_update_check.assert_called_once() + + update = hass.states.get("update.qsw_m408_4c_firmware_update") + assert update.state == STATE_UNAVAILABLE diff --git a/tests/components/qnap_qsw/test_init.py b/tests/components/qnap_qsw/test_init.py index 211cd7ed41d..75fa9ec8adc 100644 --- a/tests/components/qnap_qsw/test_init.py +++ b/tests/components/qnap_qsw/test_init.py @@ -20,6 +20,9 @@ async def test_unload_entry(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.check_firmware", + return_value=None, + ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.validate", return_value=None, ), patch( diff --git a/tests/components/qnap_qsw/test_update.py b/tests/components/qnap_qsw/test_update.py new file mode 100644 index 00000000000..69f4a3d08b4 --- /dev/null +++ b/tests/components/qnap_qsw/test_update.py @@ -0,0 +1,26 @@ +"""The sensor tests for the QNAP QSW platform.""" + +from aioqsw.const import API_RESULT, API_VERSION + +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant + +from .util import FIRMWARE_INFO_MOCK, FIRMWARE_UPDATE_CHECK_MOCK, async_init_integration + + +async def test_qnap_qsw_update(hass: HomeAssistant) -> None: + """Test creation of update entities.""" + + await async_init_integration(hass) + + update = hass.states.get("update.qsw_m408_4c_firmware_update") + assert update is not None + assert update.state == STATE_OFF + assert ( + update.attributes.get("installed_version") + == FIRMWARE_INFO_MOCK[API_RESULT][API_VERSION] + ) + assert ( + update.attributes.get("latest_version") + == FIRMWARE_UPDATE_CHECK_MOCK[API_RESULT][API_VERSION] + ) diff --git a/tests/components/qnap_qsw/util.py b/tests/components/qnap_qsw/util.py index 28e7f7881d5..a057dfbe3ac 100644 --- a/tests/components/qnap_qsw/util.py +++ b/tests/components/qnap_qsw/util.py @@ -12,6 +12,8 @@ from aioqsw.const import ( API_COMMIT_CPSS, API_COMMIT_ISS, API_DATE, + API_DESCRIPTION, + API_DOWNLOAD_URL, API_ERROR_CODE, API_ERROR_MESSAGE, API_FAN1_SPEED, @@ -20,6 +22,7 @@ from aioqsw.const import ( API_MAX_SWITCH_TEMP, API_MESSAGE, API_MODEL, + API_NEWER, API_NUMBER, API_PORT_NUM, API_PRODUCT, @@ -90,6 +93,24 @@ FIRMWARE_INFO_MOCK = { }, } +FIRMWARE_UPDATE_CHECK_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: { + API_VERSION: "1.2.0", + API_NUMBER: "29649", + API_BUILD_NUMBER: "20220128", + API_DATE: "Fri, 28 Jan 2022 01:17:39 +0800", + API_DESCRIPTION: "", + API_DOWNLOAD_URL: [ + "https://download.qnap.com/Storage/Networking/QSW408FW/QSW-M408AC3-FW.v1.2.0_S20220128_29649.img", + "https://eu1.qnap.com/Storage/Networking/QSW408FW/QSW-M408AC3-FW.v1.2.0_S20220128_29649.img", + "https://us1.qnap.com/Storage/Networking/QSW408FW/QSW-M408AC3-FW.v1.2.0_S20220128_29649.img", + ], + API_NEWER: False, + }, +} + SYSTEM_COMMAND_MOCK = { API_ERROR_CODE: 200, API_ERROR_MESSAGE: "OK", @@ -146,6 +167,9 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_info", return_value=FIRMWARE_INFO_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", + return_value=FIRMWARE_UPDATE_CHECK_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, @@ -155,6 +179,9 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_time", return_value=SYSTEM_TIME_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_users_verification", + return_value=USERS_VERIFICATION_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", return_value=USERS_LOGIN_MOCK, From 9a9fea442321c478a4be3b7b596900a67c5e9b4b Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 30 Jun 2022 22:10:19 +0300 Subject: [PATCH 041/820] Migrate `glances` unique_id to new format (#74033) --- homeassistant/components/glances/sensor.py | 46 ++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 7dfd0c503ef..e37cfaca211 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,8 +1,9 @@ """Support gathering system information of hosts which are running glances.""" from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE +from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -18,14 +19,34 @@ async def async_setup_entry( ) -> None: """Set up the Glances sensors.""" - client = hass.data[DOMAIN][config_entry.entry_id] + client: GlancesData = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] + @callback + def _migrate_old_unique_ids( + hass: HomeAssistant, old_unique_id: str, new_key: str + ) -> None: + """Migrate unique IDs to the new format.""" + ent_reg = entity_registry.async_get(hass) + + if entity_id := ent_reg.async_get_entity_id( + Platform.SENSOR, DOMAIN, old_unique_id + ): + + ent_reg.async_update_entity( + entity_id, new_unique_id=f"{config_entry.entry_id}-{new_key}" + ) + for description in SENSOR_TYPES: if description.type == "fs": # fs will provide a list of disks attached for disk in client.api.data[description.type]: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {disk['mnt_point']} {description.name_suffix}", + f"{disk['mnt_point']}-{description.key}", + ) dev.append( GlancesSensor( client, @@ -38,6 +59,11 @@ async def async_setup_entry( # sensors will provide temp for different devices for sensor in client.api.data[description.type]: if sensor["type"] == description.key: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {sensor['label']} {description.name_suffix}", + f"{sensor['label']}-{description.key}", + ) dev.append( GlancesSensor( client, @@ -48,8 +74,18 @@ async def async_setup_entry( ) elif description.type == "raid": for raid_device in client.api.data[description.type]: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {raid_device} {description.name_suffix}", + f"{raid_device}-{description.key}", + ) dev.append(GlancesSensor(client, name, raid_device, description)) elif client.api.data[description.type]: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {description.name_suffix}", + f"-{description.key}", + ) dev.append( GlancesSensor( client, @@ -87,11 +123,7 @@ class GlancesSensor(SensorEntity): manufacturer="Glances", name=name, ) - - @property - def unique_id(self): - """Set unique_id for sensor.""" - return f"{self.glances_data.host}-{self.name}" + self._attr_unique_id = f"{self.glances_data.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" @property def available(self): From 8ef87205f928948f7c2b4d10ec24788726f02650 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 21:11:45 +0200 Subject: [PATCH 042/820] Improve type hints in mqtt (#74247) --- .../components/mqtt/binary_sensor.py | 5 ++-- homeassistant/components/mqtt/config_flow.py | 30 +++++++++++++------ tests/components/mqtt/test_config_flow.py | 4 +-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 9e0a049b15e..012c6e81ac8 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -291,12 +292,12 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): return self._state @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this sensor.""" return self._config.get(CONF_DEVICE_CLASS) @property - def force_update(self): + def force_update(self) -> bool: """Force update.""" return self._config[CONF_FORCE_UPDATE] diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a8d0957921d..538c12d258c 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict import queue +from typing import Any import voluptuous as vol @@ -55,14 +56,18 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return MQTTOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return await self.async_step_broker() - async def async_step_broker(self, user_input=None): + async def async_step_broker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm the setup.""" errors = {} @@ -102,9 +107,12 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_hassio_confirm() - async def async_step_hassio_confirm(self, user_input=None): + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm a Hass.io discovery.""" errors = {} + assert self._hassio_discovery if user_input is not None: data = self._hassio_discovery @@ -148,11 +156,13 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self.broker_config: dict[str, str | int] = {} self.options = dict(config_entry.options) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: None = None) -> FlowResult: """Manage the MQTT options.""" return await self.async_step_broker() - async def async_step_broker(self, user_input=None): + async def async_step_broker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the MQTT broker configuration.""" errors = {} current_config = self.config_entry.data @@ -200,12 +210,14 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): last_step=False, ) - async def async_step_options(self, user_input=None): + async def async_step_options( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the MQTT options.""" errors = {} current_config = self.config_entry.data yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) - options_config = {} + options_config: dict[str, Any] = {} if user_input is not None: bad_birth = False bad_will = False @@ -251,7 +263,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self.hass.config_entries.async_update_entry( self.config_entry, data=updated_config ) - return self.async_create_entry(title="", data=None) + return self.async_create_entry(title="", data={}) birth = { **DEFAULT_BIRTH, @@ -269,7 +281,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY) ) - fields = OrderedDict() + fields: OrderedDict[vol.Marker, Any] = OrderedDict() fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool # Birth message is disabled if CONF_BIRTH_MESSAGE = {} diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 6fe781335f0..6f1b662e282 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -312,7 +312,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] is None + assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345, @@ -387,7 +387,7 @@ async def test_disable_birth_will( }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] is None + assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345, From 7656ca8313a7b423ad49fdbc677d66e6edf90582 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 30 Jun 2022 21:12:12 +0200 Subject: [PATCH 043/820] Add presence detection to devolo_home_network (#72030) --- .../components/devolo_home_network/const.py | 13 +- .../devolo_home_network/device_tracker.py | 159 ++++++++++++++++++ .../devolo_home_network/__init__.py | 4 +- tests/components/devolo_home_network/const.py | 4 + .../test_device_tracker.py | 107 ++++++++++++ 5 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/devolo_home_network/device_tracker.py create mode 100644 tests/components/devolo_home_network/test_device_tracker.py diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index 2dfdd3c1d9a..1a49beb5d02 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -5,8 +5,9 @@ from datetime import timedelta from homeassistant.const import Platform DOMAIN = "devolo_home_network" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] +MAC_ADDRESS = "mac_address" PRODUCT = "product" SERIAL_NUMBER = "serial_number" TITLE = "title" @@ -15,6 +16,16 @@ LONG_UPDATE_INTERVAL = timedelta(minutes=5) SHORT_UPDATE_INTERVAL = timedelta(seconds=15) CONNECTED_PLC_DEVICES = "connected_plc_devices" +CONNECTED_STATIONS = "connected_stations" CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" + +WIFI_APTYPE = { + "WIFI_VAP_MAIN_AP": "Main", + "WIFI_VAP_GUEST_AP": "Guest", +} +WIFI_BANDS = { + "WIFI_BAND_2G": 2.4, + "WIFI_BAND_5G": 5, +} diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py new file mode 100644 index 00000000000..9dffeef7db9 --- /dev/null +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -0,0 +1,159 @@ +"""Platform for device tracker integration.""" +from __future__ import annotations + +from typing import Any + +from devolo_plc_api.device import Device + +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, + SOURCE_TYPE_ROUTER, +) +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import FREQUENCY_GIGAHERTZ, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + CONNECTED_STATIONS, + CONNECTED_WIFI_CLIENTS, + DOMAIN, + MAC_ADDRESS, + WIFI_APTYPE, + WIFI_BANDS, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id][ + "coordinators" + ] + registry = entity_registry.async_get(hass) + tracked = set() + + @callback + def new_device_callback() -> None: + """Add new devices if needed.""" + new_entities = [] + for station in coordinators[CONNECTED_WIFI_CLIENTS].data[CONNECTED_STATIONS]: + if station[MAC_ADDRESS] in tracked: + continue + + new_entities.append( + DevoloScannerEntity( + coordinators[CONNECTED_WIFI_CLIENTS], device, station[MAC_ADDRESS] + ) + ) + tracked.add(station[MAC_ADDRESS]) + if new_entities: + async_add_entities(new_entities) + + @callback + def restore_entities() -> None: + """Restore clients that are not a part of active clients list.""" + missing = [] + for entity in entity_registry.async_entries_for_config_entry( + registry, entry.entry_id + ): + if ( + entity.platform == DOMAIN + and entity.domain == DEVICE_TRACKER_DOMAIN + and ( + mac_address := entity.unique_id.replace( + f"{device.serial_number}_", "" + ) + ) + not in tracked + ): + missing.append( + DevoloScannerEntity( + coordinators[CONNECTED_WIFI_CLIENTS], device, mac_address + ) + ) + tracked.add(mac_address) + + if missing: + async_add_entities(missing) + + if device.device and "wifi1" in device.device.features: + restore_entities() + entry.async_on_unload( + coordinators[CONNECTED_WIFI_CLIENTS].async_add_listener(new_device_callback) + ) + + +class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): + """Representation of a devolo device tracker.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, device: Device, mac: str + ) -> None: + """Initialize entity.""" + super().__init__(coordinator) + self._device = device + self._mac = mac + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the attributes.""" + attrs: dict[str, str] = {} + if not self.coordinator.data[CONNECTED_STATIONS]: + return {} + + station: dict[str, Any] = next( + ( + station + for station in self.coordinator.data[CONNECTED_STATIONS] + if station[MAC_ADDRESS] == self.mac_address + ), + {}, + ) + if station: + attrs["wifi"] = WIFI_APTYPE.get(station["vap_type"], STATE_UNKNOWN) + attrs["band"] = ( + f"{WIFI_BANDS.get(station['band'])} {FREQUENCY_GIGAHERTZ}" + if WIFI_BANDS.get(station["band"]) + else STATE_UNKNOWN + ) + return attrs + + @property + def icon(self) -> str: + """Return device icon.""" + if self.is_connected: + return "mdi:lan-connect" + return "mdi:lan-disconnect" + + @property + def is_connected(self) -> bool: + """Return true if the device is connected to the network.""" + return any( + station + for station in self.coordinator.data[CONNECTED_STATIONS] + if station[MAC_ADDRESS] == self.mac_address + ) + + @property + def mac_address(self) -> str: + """Return mac_address.""" + return self._mac + + @property + def source_type(self) -> str: + """Return tracker source type.""" + return SOURCE_TYPE_ROUTER + + @property + def unique_id(self) -> str: + """Return unique ID of the entity.""" + return f"{self._device.serial_number}_{self._mac}" diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index 1c10d7a59ef..bb861081517 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -28,6 +28,8 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: async def async_connect(self, session_instance: Any = None): """Give a mocked device the needed properties.""" - self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] + self.product = DISCOVERY_INFO.properties["Product"] + self.serial_number = DISCOVERY_INFO.properties["SN"] diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index 516a19f3421..aec27ce6a8b 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -16,6 +16,10 @@ CONNECTED_STATIONS = { ], } +NO_CONNECTED_STATIONS = { + "connected_stations": [], +} + DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host=IP, addresses=[IP], diff --git a/tests/components/devolo_home_network/test_device_tracker.py b/tests/components/devolo_home_network/test_device_tracker.py new file mode 100644 index 00000000000..233a480b5e3 --- /dev/null +++ b/tests/components/devolo_home_network/test_device_tracker.py @@ -0,0 +1,107 @@ +"""Tests for the devolo Home Network device tracker.""" +from unittest.mock import AsyncMock, patch + +from devolo_plc_api.exceptions.device import DeviceUnavailable +import pytest + +from homeassistant.components.device_tracker import DOMAIN as PLATFORM +from homeassistant.components.devolo_home_network.const import ( + DOMAIN, + LONG_UPDATE_INTERVAL, + WIFI_APTYPE, + WIFI_BANDS, +) +from homeassistant.const import ( + FREQUENCY_GIGAHERTZ, + STATE_HOME, + STATE_NOT_HOME, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.util import dt + +from . import configure_integration +from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NO_CONNECTED_STATIONS + +from tests.common import async_fire_time_changed + +STATION = CONNECTED_STATIONS["connected_stations"][0] +SERIAL = DISCOVERY_INFO.properties["SN"] + + +@pytest.mark.usefixtures("mock_device") +async def test_device_tracker(hass: HomeAssistant): + """Test device tracker states.""" + state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + entry = configure_integration(hass) + er = entity_registry.async_get(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + # Enable entity + er.async_update_entity(state_key, disabled_by=None) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_HOME + assert state.attributes["wifi"] == WIFI_APTYPE[STATION["vap_type"]] + assert ( + state.attributes["band"] + == f"{WIFI_BANDS[STATION['band']]} {FREQUENCY_GIGAHERTZ}" + ) + + # Emulate state change + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + new=AsyncMock(return_value=NO_CONNECTED_STATIONS), + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_NOT_HOME + + # Emulate device failure + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +async def test_restoring_clients(hass: HomeAssistant): + """Test restoring existing device_tracker entities.""" + state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + entry = configure_integration(hass) + er = entity_registry.async_get(hass) + er.async_get_or_create( + PLATFORM, + DOMAIN, + f"{SERIAL}_{STATION['mac_address']}", + config_entry=entry, + ) + + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + new=AsyncMock(return_value=NO_CONNECTED_STATIONS), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_NOT_HOME From 24d2d6212135bea2e1a2e8d8f35d967001124811 Mon Sep 17 00:00:00 2001 From: mbo18 Date: Thu, 30 Jun 2022 21:22:03 +0200 Subject: [PATCH 044/820] Add vacation mode to Template Alarm Control Panel (#74261) --- .../components/template/alarm_control_panel.py | 18 ++++++++++++++++++ .../template/test_alarm_control_panel.py | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index ae26e58ac04..132e9fb0ca5 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -21,6 +21,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -43,6 +44,7 @@ _VALID_STATES = [ STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -53,6 +55,7 @@ _VALID_STATES = [ CONF_ARM_AWAY_ACTION = "arm_away" CONF_ARM_HOME_ACTION = "arm_home" CONF_ARM_NIGHT_ACTION = "arm_night" +CONF_ARM_VACATION_ACTION = "arm_vacation" CONF_DISARM_ACTION = "disarm" CONF_ALARM_CONTROL_PANELS = "panels" CONF_CODE_ARM_REQUIRED = "code_arm_required" @@ -74,6 +77,7 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum( TemplateCodeFormat @@ -157,6 +161,9 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): self._arm_night_script = None if (arm_night_action := config.get(CONF_ARM_NIGHT_ACTION)) is not None: self._arm_night_script = Script(hass, arm_night_action, name, DOMAIN) + self._arm_vacation_script = None + if (arm_vacation_action := config.get(CONF_ARM_VACATION_ACTION)) is not None: + self._arm_vacation_script = Script(hass, arm_vacation_action, name, DOMAIN) self._state: str | None = None @@ -184,6 +191,11 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): supported_features | AlarmControlPanelEntityFeature.ARM_AWAY ) + if self._arm_vacation_script is not None: + supported_features = ( + supported_features | AlarmControlPanelEntityFeature.ARM_VACATION + ) + return supported_features @property @@ -257,6 +269,12 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): STATE_ALARM_ARMED_NIGHT, script=self._arm_night_script, code=code ) + async def async_alarm_arm_vacation(self, code: str | None = None) -> None: + """Arm the panel to Vacation.""" + await self._async_alarm_arm( + STATE_ALARM_ARMED_VACATION, script=self._arm_vacation_script, code=code + ) + async def async_alarm_disarm(self, code: str | None = None) -> None: """Disarm the panel.""" await self._async_alarm_arm( diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 8f9ab39f7c0..9e51b48dcc6 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -10,6 +10,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -56,6 +57,11 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = { "entity_id": "alarm_control_panel.test", "data": {"code": "{{ this.entity_id }}"}, }, + "arm_vacation": { + "service": "alarm_control_panel.alarm_arm_vacation", + "entity_id": "alarm_control_panel.test", + "data": {"code": "{{ this.entity_id }}"}, + }, "disarm": { "service": "alarm_control_panel.alarm_disarm", "entity_id": "alarm_control_panel.test", @@ -89,6 +95,7 @@ async def test_template_state_text(hass, start_ha): STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -128,6 +135,7 @@ async def test_optimistic_states(hass, start_ha): ("alarm_arm_away", STATE_ALARM_ARMED_AWAY), ("alarm_arm_home", STATE_ALARM_ARMED_HOME), ("alarm_arm_night", STATE_ALARM_ARMED_NIGHT), + ("alarm_arm_vacation", STATE_ALARM_ARMED_VACATION), ("alarm_disarm", STATE_ALARM_DISARMED), ]: await hass.services.async_call( @@ -250,6 +258,7 @@ async def test_name(hass, start_ha): "alarm_arm_home", "alarm_arm_away", "alarm_arm_night", + "alarm_arm_vacation", "alarm_disarm", ], ) From f80d522c6a68451a1d1fbe6bd532d0658393094f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 21:50:57 +0200 Subject: [PATCH 045/820] Add Camera checks to pylint plugin (#74264) --- pylint/plugins/hass_enforce_type_hints.py | 114 ++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 2696c9965e4..36c198c7218 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -742,6 +742,120 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "camera": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="Camera", + matches=[ + TypeHintMatch( + function_name="entity_picture", + return_type="str", + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="is_recording", + return_type="bool", + ), + TypeHintMatch( + function_name="is_streaming", + return_type="bool", + ), + TypeHintMatch( + function_name="brand", + return_type=["str", None], + ), + TypeHintMatch( + function_name="motion_detection_enabled", + return_type="bool", + ), + TypeHintMatch( + function_name="model", + return_type=["str", None], + ), + TypeHintMatch( + function_name="frame_interval", + return_type="float", + ), + TypeHintMatch( + function_name="frontend_stream_type", + return_type=["StreamType", None], + ), + TypeHintMatch( + function_name="available", + return_type="bool", + ), + TypeHintMatch( + function_name="async_create_stream", + return_type=["Stream", None], + ), + TypeHintMatch( + function_name="stream_source", + return_type=["str", None], + ), + TypeHintMatch( + function_name="async_handle_web_rtc_offer", + arg_types={ + 1: "str", + }, + return_type=["str", None], + ), + TypeHintMatch( + function_name="camera_image", + named_arg_types={ + "width": "int | None", + "height": "int | None", + }, + return_type=["bytes", None], + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="handle_async_still_stream", + arg_types={ + 1: "Request", + 2: "float", + }, + return_type="StreamResponse", + ), + TypeHintMatch( + function_name="handle_async_mjpeg_stream", + arg_types={ + 1: "Request", + }, + return_type=["StreamResponse", None], + ), + TypeHintMatch( + function_name="is_on", + return_type="bool", + ), + TypeHintMatch( + function_name="turn_off", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_on", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="enable_motion_detection", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="disable_motion_detection", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", From dc559f2439df34d9cec1b4c284fb07bde7ab5899 Mon Sep 17 00:00:00 2001 From: Christopher Hoage Date: Thu, 30 Jun 2022 13:06:22 -0700 Subject: [PATCH 046/820] Bump venstarcolortouch to 0.17 (#74271) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index e63c75792bf..2f3331af6e2 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.16"], + "requirements": ["venstarcolortouch==0.17"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index eac0800e73a..6a515ec37e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,7 +2387,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.16 +venstarcolortouch==0.17 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4b5e587f7a..8c3f01ecca7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1593,7 +1593,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.16 +venstarcolortouch==0.17 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From 269fa1472105ed8b841e4f5d1f671563dce2c5e4 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 30 Jun 2022 16:59:35 -0400 Subject: [PATCH 047/820] Fix bad conditional in ZHA logbook (#74277) * Fix bad conditional in ZHA logbook * change syntax --- homeassistant/components/zha/logbook.py | 4 ++-- tests/components/zha/test_logbook.py | 29 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index 8140a5244f1..90d433be210 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -74,8 +74,8 @@ def async_describe_events( else: message = f"{event_type} event was fired" - if event_data["params"]: - message = f"{message} with parameters: {event_data['params']}" + if params := event_data.get("params"): + message = f"{message} with parameters: {params}" return { LOGBOOK_ENTRY_NAME: device_name, diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 6c28284b1e6..373a48c2d47 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -185,6 +185,27 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): }, }, ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": {}, + }, + ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + }, + ), ], ) @@ -201,6 +222,14 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): events[1]["message"] == "Zha Event was fired with parameters: {'test': 'test'}" ) + assert events[2]["name"] == "FakeManufacturer FakeModel" + assert events[2]["domain"] == "zha" + assert events[2]["message"] == "Zha Event was fired" + + assert events[3]["name"] == "FakeManufacturer FakeModel" + assert events[3]["domain"] == "zha" + assert events[3]["message"] == "Zha Event was fired" + async def test_zha_logbook_event_device_no_device(hass, mock_devices): """Test zha logbook events without device and without triggers.""" From 4f842014ee96506c3198a2da373b91067b8eec9d Mon Sep 17 00:00:00 2001 From: Khole Date: Thu, 30 Jun 2022 22:47:02 +0100 Subject: [PATCH 048/820] Add ability to forget hive device when removing integration (#74144) --- homeassistant/components/hive/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 52cf7f719e6..3693f0183ce 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -7,7 +7,7 @@ import logging from typing import Any, TypeVar from aiohttp.web_exceptions import HTTPException -from apyhiveapi import Hive +from apyhiveapi import Auth, Hive from apyhiveapi.helper.hive_exceptions import HiveReauthRequired from typing_extensions import Concatenate, ParamSpec import voluptuous as vol @@ -112,6 +112,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove a config entry.""" + hive = Auth(entry.data["username"], entry.data["password"]) + await hive.forget_device( + entry.data["tokens"]["AuthenticationResult"]["AccessToken"], + entry.data["device_data"][1], + ) + + def refresh_system( func: Callable[Concatenate[_HiveEntityT, _P], Awaitable[Any]] ) -> Callable[Concatenate[_HiveEntityT, _P], Coroutine[Any, Any, None]]: From 11cdf542ac169bf72c4a0fb7b06693c9c1e91cfb Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 30 Jun 2022 23:48:50 +0200 Subject: [PATCH 049/820] Bump pyRFXtrx to 0.30.0 (#74146) --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rfxtrx/test_config_flow.py | 12 ++++-- tests/components/rfxtrx/test_cover.py | 37 ++++++++++--------- tests/components/rfxtrx/test_switch.py | 4 +- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index cfe1049c888..3439fbba70c 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.29.0"], + "requirements": ["pyRFXtrx==0.30.0"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 6a515ec37e6..0cb6d5122e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.29.0 +pyRFXtrx==0.30.0 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c3f01ecca7..4d60f632108 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -917,7 +917,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.29.0 +pyRFXtrx==0.30.0 # homeassistant.components.tibber pyTibber==0.22.3 diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index a756bf26b9f..2c695d71d2e 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -847,7 +847,7 @@ async def test_options_configure_rfy_cover_device(hass): result["flow_id"], user_input={ "automatic_add": True, - "event_code": "071a000001020301", + "event_code": "0C1a0000010203010000000000", }, ) @@ -863,7 +863,10 @@ async def test_options_configure_rfy_cover_device(hass): await hass.async_block_till_done() - assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + assert ( + entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] + == "EU" + ) device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) @@ -897,7 +900,10 @@ async def test_options_configure_rfy_cover_device(hass): await hass.async_block_till_done() - assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + assert ( + entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] + == "EU" + ) def test_get_serial_by_id_no_dir(): diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index e3d44edda82..3be41d9233e 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -146,8 +146,11 @@ async def test_rfy_cover(hass, rfxtrx): "071a000001020301": { "venetian_blind_mode": "Unknown", }, - "071a000001020302": {"venetian_blind_mode": "US"}, - "071a000001020303": {"venetian_blind_mode": "EU"}, + "0c1a0000010203010000000000": { + "venetian_blind_mode": "Unknown", + }, + "0c1a0000010203020000000000": {"venetian_blind_mode": "US"}, + "0c1a0000010203030000000000": {"venetian_blind_mode": "EU"}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -199,9 +202,9 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x01\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x01\x01")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x01\x03")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x01\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x01\x01\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x01\x03\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to US @@ -252,12 +255,12 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x02\x0F")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x02\x10")), - call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x02\x11")), - call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x02\x12")), - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x02\x0F\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x02\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x02\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x02\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to EU @@ -308,10 +311,10 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x03\x11")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x03\x12")), - call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x03\x0F")), - call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x03\x10")), - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x03\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x03\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x03\x0F\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x03\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), ] diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 4da7f1d9881..4d92c6fa332 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -11,8 +11,8 @@ from homeassistant.core import State from tests.common import MockConfigEntry, mock_restore_cache from tests.components.rfxtrx.conftest import create_rfx_test_cfg -EVENT_RFY_ENABLE_SUN_AUTO = "081a00000301010113" -EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114" +EVENT_RFY_ENABLE_SUN_AUTO = "0C1a0000030101011300000003" +EVENT_RFY_DISABLE_SUN_AUTO = "0C1a0000030101011400000003" async def test_one_switch(hass, rfxtrx): From 2c171e30fa32a85d8a6a5cd3f3a40433b384c094 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 23:49:23 +0200 Subject: [PATCH 050/820] Add ClimateEntity checks to pylint plugin (#74275) * Add ClimateEntity checks to pylint plugin * Update pylint/plugins/hass_enforce_type_hints.py --- pylint/plugins/hass_enforce_type_hints.py | 173 ++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 36c198c7218..8e846cf5db0 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -856,6 +856,179 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "climate": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ClimateEntity", + matches=[ + TypeHintMatch( + function_name="precision", + return_type="float", + ), + TypeHintMatch( + function_name="temperature_unit", + return_type="str", + ), + TypeHintMatch( + function_name="current_humidity", + return_type=["int", None], + ), + TypeHintMatch( + function_name="target_humidity", + return_type=["int", None], + ), + TypeHintMatch( + function_name="hvac_mode", + return_type=["HVACMode", "str", None], + ), + TypeHintMatch( + function_name="hvac_modes", + return_type=["list[HVACMode]", "list[str]"], + ), + TypeHintMatch( + function_name="hvac_action", + return_type=["HVACAction", "str", None], + ), + TypeHintMatch( + function_name="current_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_step", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_high", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_low", + return_type=["float", None], + ), + TypeHintMatch( + function_name="preset_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="preset_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="is_aux_heat", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="fan_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="fan_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="swing_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="swing_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="set_temperature", + kwargs_type="Any", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_humidity", + arg_types={ + 1: "int", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_fan_mode", + arg_types={ + 1: "str", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_hvac_mode", + arg_types={ + 1: "HVACMode", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_swing_mode", + arg_types={ + 1: "str", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_preset_mode", + arg_types={ + 1: "str", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_aux_heat_on", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_aux_heat_off", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_on", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_off", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="min_temp", + return_type="float", + ), + TypeHintMatch( + function_name="max_temp", + return_type="float", + ), + TypeHintMatch( + function_name="min_humidity", + return_type="int", + ), + TypeHintMatch( + function_name="max_humidity", + return_type="int", + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", From 73a0197cac501c428bdf54bd23fe70f7e80b4b8d Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Thu, 30 Jun 2022 23:55:57 +0200 Subject: [PATCH 051/820] Elmax/sensor platform (#64090) --- .coveragerc | 1 + .../components/elmax/binary_sensor.py | 68 +++++++++++++++++++ homeassistant/components/elmax/common.py | 6 ++ homeassistant/components/elmax/const.py | 2 +- 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/elmax/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 2443e30a2c3..f12d1ce88fa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -276,6 +276,7 @@ omit = homeassistant/components/elmax/__init__.py homeassistant/components/elmax/common.py homeassistant/components/elmax/const.py + homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/switch.py homeassistant/components/elv/* homeassistant/components/emby/media_player.py diff --git a/homeassistant/components/elmax/binary_sensor.py b/homeassistant/components/elmax/binary_sensor.py new file mode 100644 index 00000000000..71588b4687f --- /dev/null +++ b/homeassistant/components/elmax/binary_sensor.py @@ -0,0 +1,68 @@ +"""Elmax sensor platform.""" +from __future__ import annotations + +from elmax_api.model.panel import PanelStatus + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ElmaxCoordinator +from .common import ElmaxEntity +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Elmax sensor platform.""" + coordinator: ElmaxCoordinator = hass.data[DOMAIN][config_entry.entry_id] + known_devices = set() + + def _discover_new_devices(): + panel_status: PanelStatus = coordinator.data + # In case the panel is offline, its status will be None. In that case, simply do nothing + if panel_status is None: + return + + # Otherwise, add all the entities we found + entities = [] + for zone in panel_status.zones: + # Skip already handled devices + if zone.endpoint_id in known_devices: + continue + entity = ElmaxSensor( + panel=coordinator.panel_entry, + elmax_device=zone, + panel_version=panel_status.release, + coordinator=coordinator, + ) + entities.append(entity) + async_add_entities(entities, True) + known_devices.update([e.unique_id for e in entities]) + + # Register a listener for the discovery of new devices + coordinator.async_add_listener(_discover_new_devices) + + # Immediately run a discovery, so we don't need to wait for the next update + _discover_new_devices() + + +class ElmaxSensor(ElmaxEntity, BinarySensorEntity): + """Elmax Sensor entity implementation.""" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.coordinator.get_zone_state(self._device.endpoint_id).opened + + @property + def device_class(self) -> BinarySensorDeviceClass: + """Return the class of this device, from component DEVICE_CLASSES.""" + return BinarySensorDeviceClass.DOOR diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index 2d66ca9f72e..4116ff05f44 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -65,6 +65,12 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): return self._state_by_endpoint.get(actuator_id) raise HomeAssistantError("Unknown actuator") + def get_zone_state(self, zone_id: str) -> Actuator: + """Return state of a specific zone.""" + if self._state_by_endpoint is not None: + return self._state_by_endpoint.get(zone_id) + raise HomeAssistantError("Unknown zone") + @property def http_client(self): """Return the current http client being used by this instance.""" diff --git a/homeassistant/components/elmax/const.py b/homeassistant/components/elmax/const.py index 21864e98f1a..514412d6897 100644 --- a/homeassistant/components/elmax/const.py +++ b/homeassistant/components/elmax/const.py @@ -11,7 +11,7 @@ CONF_ELMAX_PANEL_NAME = "panel_name" CONF_CONFIG_ENTRY_ID = "config_entry_id" CONF_ENDPOINT_ID = "endpoint_id" -ELMAX_PLATFORMS = [Platform.SWITCH] +ELMAX_PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR] POLLING_SECONDS = 30 DEFAULT_TIMEOUT = 10.0 From 7eae3691c2d219b30a9c2a7d964cf82f6e8d1798 Mon Sep 17 00:00:00 2001 From: rappenze Date: Thu, 30 Jun 2022 23:57:35 +0200 Subject: [PATCH 052/820] Add device info to fibaro integration (#73352) --- homeassistant/components/fibaro/__init__.py | 74 +++++++++++++++++-- .../components/fibaro/config_flow.py | 8 +- homeassistant/components/fibaro/scene.py | 10 +++ tests/components/fibaro/test_config_flow.py | 12 ++- 4 files changed, 93 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index bd7c3a09ec0..3b5eece1a14 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -29,8 +29,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -156,16 +157,21 @@ class FibaroController: ) # List of devices by entity platform self._callbacks: dict[Any, Any] = {} # Update value callbacks by deviceId self._state_handler = None # Fiblary's StateHandler object - self.hub_serial = None # Unique serial number of the hub - self.name = None # The friendly name of the hub + self.hub_serial: str # Unique serial number of the hub + self.hub_name: str # The friendly name of the hub + self.hub_software_version: str + self.hub_api_url: str = config[CONF_URL] + # Device infos by fibaro device id + self._device_infos: dict[int, DeviceInfo] = {} def connect(self): """Start the communication with the Fibaro controller.""" try: login = self._client.login.get() info = self._client.info.get() - self.hub_serial = slugify(info.serialNumber) - self.name = slugify(info.hcName) + self.hub_serial = info.serialNumber + self.hub_name = info.hcName + self.hub_software_version = info.softVersion except AssertionError: _LOGGER.error("Can't connect to Fibaro HC. Please check URL") return False @@ -305,6 +311,44 @@ class FibaroController: platform = Platform.LIGHT return platform + def _create_device_info(self, device: Any, devices: list) -> None: + """Create the device info. Unrooted entities are directly shown below the home center.""" + + # The home center is always id 1 (z-wave primary controller) + if "parentId" not in device or device.parentId <= 1: + return + + master_entity: Any | None = None + if device.parentId == 1: + master_entity = device + else: + for parent in devices: + if "id" in parent and parent.id == device.parentId: + master_entity = parent + if master_entity is None: + _LOGGER.error("Parent with id %s not found", device.parentId) + return + + if "zwaveCompany" in master_entity.properties: + manufacturer = master_entity.properties.zwaveCompany + else: + manufacturer = "Unknown" + + self._device_infos[master_entity.id] = DeviceInfo( + identifiers={(DOMAIN, master_entity.id)}, + manufacturer=manufacturer, + name=master_entity.name, + via_device=(DOMAIN, self.hub_serial), + ) + + def get_device_info(self, device: Any) -> DeviceInfo: + """Get the device info by fibaro device id.""" + if device.id in self._device_infos: + return self._device_infos[device.id] + if "parentId" in device and device.parentId in self._device_infos: + return self._device_infos[device.parentId] + return DeviceInfo(identifiers={(DOMAIN, self.hub_serial)}) + def _read_scenes(self): scenes = self._client.scenes.list() self._scene_map = {} @@ -321,14 +365,14 @@ class FibaroController: device.ha_id = ( f"scene_{slugify(room_name)}_{slugify(device.name)}_{device.id}" ) - device.unique_id_str = f"{self.hub_serial}.scene.{device.id}" + device.unique_id_str = f"{slugify(self.hub_serial)}.scene.{device.id}" self._scene_map[device.id] = device self.fibaro_devices[Platform.SCENE].append(device) _LOGGER.debug("%s scene -> %s", device.ha_id, device) def _read_devices(self): """Read and process the device list.""" - devices = self._client.devices.list() + devices = list(self._client.devices.list()) self._device_map = {} last_climate_parent = None last_endpoint = None @@ -355,7 +399,8 @@ class FibaroController: device.mapped_platform = None if (platform := device.mapped_platform) is None: continue - device.unique_id_str = f"{self.hub_serial}.{device.id}" + device.unique_id_str = f"{slugify(self.hub_serial)}.{device.id}" + self._create_device_info(device, devices) self._device_map[device.id] = device _LOGGER.debug( "%s (%s, %s) -> %s %s", @@ -462,6 +507,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for platform in PLATFORMS: devices[platform] = [*controller.fibaro_devices[platform]] + # register the hub device info separately as the hub has sometimes no entities + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, controller.hub_serial)}, + manufacturer="Fibaro", + name=controller.hub_name, + model=controller.hub_serial, + sw_version=controller.hub_software_version, + configuration_url=controller.hub_api_url.removesuffix("/api/"), + ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) controller.enable_state_handler() @@ -490,6 +547,7 @@ class FibaroDevice(Entity): self.ha_id = fibaro_device.ha_id self._attr_name = fibaro_device.friendly_name self._attr_unique_id = fibaro_device.unique_id_str + self._attr_device_info = self.controller.get_device_info(fibaro_device) # propagate hidden attribute set in fibaro home center to HA if "visible" in fibaro_device and fibaro_device.visible is False: self._attr_entity_registry_visible_default = False diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index b0ea05e49e1..fd53bd5b94f 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any +from slugify import slugify import voluptuous as vol from homeassistant import config_entries @@ -44,9 +45,12 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str _LOGGER.debug( "Successfully connected to fibaro home center %s with name %s", controller.hub_serial, - controller.name, + controller.hub_name, ) - return {"serial_number": controller.hub_serial, "name": controller.name} + return { + "serial_number": slugify(controller.hub_serial), + "name": controller.hub_name, + } class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index e4e8b19d308..045adce5764 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -7,6 +7,7 @@ from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FIBARO_DEVICES, FibaroDevice @@ -33,6 +34,15 @@ async def async_setup_entry( class FibaroScene(FibaroDevice, Scene): """Representation of a Fibaro scene entity.""" + def __init__(self, fibaro_device: Any) -> None: + """Initialize the Fibaro scene.""" + super().__init__(fibaro_device) + + # All scenes are shown on hub device + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.controller.hub_serial)} + ) + def activate(self, **kwargs: Any) -> None: """Activate the scene.""" self.fibaro_device.start() diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index f056f484a58..14f28257588 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -14,17 +14,23 @@ TEST_NAME = "my_fibaro_home_center" TEST_URL = "http://192.168.1.1/api/" TEST_USERNAME = "user" TEST_PASSWORD = "password" +TEST_VERSION = "4.360" @pytest.fixture(name="fibaro_client", autouse=True) def fibaro_client_fixture(): """Mock common methods and attributes of fibaro client.""" info_mock = Mock() - info_mock.get.return_value = Mock(serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME) + info_mock.get.return_value = Mock( + serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME, softVersion=TEST_VERSION + ) array_mock = Mock() array_mock.list.return_value = [] + client_mock = Mock() + client_mock.base_url.return_value = TEST_URL + with patch("fiblary3.client.v4.client.Client.__init__", return_value=None,), patch( "fiblary3.client.v4.client.Client.info", info_mock, @@ -37,6 +43,10 @@ def fibaro_client_fixture(): "fiblary3.client.v4.client.Client.scenes", array_mock, create=True, + ), patch( + "fiblary3.client.v4.client.Client.client", + client_mock, + create=True, ): yield From 3970639c3410394631aeb2d3d2f56a86873da802 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 1 Jul 2022 00:27:03 +0000 Subject: [PATCH 053/820] [ci skip] Translation update --- .../components/anthemav/translations/de.json | 19 ++++++++++++++ .../components/anthemav/translations/et.json | 19 ++++++++++++++ .../components/anthemav/translations/fr.json | 2 +- .../anthemav/translations/zh-Hant.json | 19 ++++++++++++++ .../components/braviatv/translations/de.json | 2 +- .../components/deconz/translations/de.json | 2 +- .../components/deluge/translations/de.json | 2 +- .../components/esphome/translations/de.json | 4 +-- .../components/esphome/translations/en.json | 4 +-- .../components/esphome/translations/fr.json | 4 +-- .../forked_daapd/translations/de.json | 2 +- .../components/lcn/translations/et.json | 1 + .../lg_soundbar/translations/de.json | 18 +++++++++++++ .../lg_soundbar/translations/fr.json | 18 +++++++++++++ .../components/life360/translations/de.json | 2 +- .../components/life360/translations/el.json | 23 +++++++++++++++++ .../components/life360/translations/et.json | 23 +++++++++++++++++ .../components/life360/translations/ja.json | 23 +++++++++++++++++ .../components/life360/translations/nl.json | 9 +++++++ .../components/life360/translations/no.json | 25 ++++++++++++++++++- .../components/mutesync/translations/de.json | 2 +- .../components/nina/translations/et.json | 22 ++++++++++++++++ .../components/nina/translations/ja.json | 1 + .../components/nina/translations/nl.json | 11 ++++++++ .../components/nina/translations/no.json | 22 ++++++++++++++++ .../components/owntracks/translations/de.json | 2 +- .../simplepush/translations/nl.json | 3 +++ .../smartthings/translations/de.json | 4 +-- .../components/starline/translations/de.json | 2 +- .../components/threshold/translations/sv.json | 3 ++- .../components/zwave_me/translations/de.json | 2 +- 31 files changed, 275 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/de.json create mode 100644 homeassistant/components/anthemav/translations/et.json create mode 100644 homeassistant/components/anthemav/translations/zh-Hant.json create mode 100644 homeassistant/components/lg_soundbar/translations/de.json create mode 100644 homeassistant/components/lg_soundbar/translations/fr.json diff --git a/homeassistant/components/anthemav/translations/de.json b/homeassistant/components/anthemav/translations/de.json new file mode 100644 index 00000000000..622384629fe --- /dev/null +++ b/homeassistant/components/anthemav/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "cannot_receive_deviceinfo": "MAC-Adresse konnte nicht abgerufen werden. Stelle sicher, dass das Ger\u00e4t eingeschaltet ist." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/et.json b/homeassistant/components/anthemav/translations/et.json new file mode 100644 index 00000000000..4ec356c8902 --- /dev/null +++ b/homeassistant/components/anthemav/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "cannot_receive_deviceinfo": "MAC-aadressi toomine eba\u00f5nnestus. Veendu, et seade oleks sisse l\u00fclitatud" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/fr.json b/homeassistant/components/anthemav/translations/fr.json index cf1e7c7aded..faf417552ce 100644 --- a/homeassistant/components/anthemav/translations/fr.json +++ b/homeassistant/components/anthemav/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "cannot_receive_deviceinfo": "Erreur lors de la d\u00e9couverte de l'addresse MAC. V\u00e9rifiez que l'appareil est allum\u00e9." + "cannot_receive_deviceinfo": "\u00c9chec de r\u00e9cup\u00e9ration de l'adresse MAC. Assurez-vous que l'appareil est allum\u00e9" }, "step": { "user": { diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json new file mode 100644 index 00000000000..d1b286afd81 --- /dev/null +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "cannot_receive_deviceinfo": "\u63a5\u6536 MAC \u4f4d\u5740\u5931\u6557\uff0c\u8acb\u78ba\u5b9a\u88dd\u7f6e\u70ba\u958b\u555f\u72c0Address. Make sure the device is turned on" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index ff00828c0f3..035de7bc060 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -14,7 +14,7 @@ "data": { "pin": "PIN-Code" }, - "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe daf\u00fcr zu: Einstellungen -> Netzwerk -> Remote - Ger\u00e4teeinstellungen -> Registrierung des entfernten Ger\u00e4ts aufheben.", + "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe daf\u00fcr zu: Einstellungen \u2192 Netzwerk \u2192 Remote - Ger\u00e4teeinstellungen \u2192 Registrierung des entfernten Ger\u00e4ts aufheben.", "title": "Autorisiere Sony Bravia TV" }, "user": { diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index 29b322466d5..b8b260709e8 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -18,7 +18,7 @@ "title": "deCONZ Zigbee Gateway \u00fcber das Supervisor Add-on" }, "link": { - "description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", + "description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \u2192 Gateway \u2192 Erweitert\n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", "title": "Mit deCONZ verbinden" }, "manual_input": { diff --git a/homeassistant/components/deluge/translations/de.json b/homeassistant/components/deluge/translations/de.json index 9e8d559a523..4fa07b82d0f 100644 --- a/homeassistant/components/deluge/translations/de.json +++ b/homeassistant/components/deluge/translations/de.json @@ -16,7 +16,7 @@ "username": "Benutzername", "web_port": "Webport (f\u00fcr Besuchsdienste)" }, - "description": "Um diese Integration nutzen zu k\u00f6nnen, musst du die folgende Option in den Deluge-Einstellungen aktivieren: Daemon > Fernsteuerungen zulassen" + "description": "Um diese Integration nutzen zu k\u00f6nnen, musst du die folgende Option in den Deluge-Einstellungen aktivieren: Daemon \u2192 Fernsteuerungen zulassen" } } } diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 6229c09a03e..7556739ce93 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -9,7 +9,7 @@ "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_psk": "Der Transportverschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig. Bitte stelle sicher, dass es mit deiner Konfiguration \u00fcbereinstimmt", - "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege bitte eine statische IP-Adresse fest" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Port" }, - "description": "Bitte gib die Verbindungseinstellungen deines [ESPHome](https://esphomelib.com/)-Knotens ein." + "description": "Bitte gib die Verbindungseinstellungen deines [ESPHome]( {esphome_url} )-Knotens ein." } } } diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index 5ca5c03f8e9..b0b502631df 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -9,7 +9,7 @@ "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", "invalid_auth": "Invalid authentication", "invalid_psk": "The transport encryption key is invalid. Please ensure it matches what you have in your configuration", - "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Port" }, - "description": "Please enter connection settings of your [ESPHome](https://esphomelib.com/) node." + "description": "Please enter connection settings of your [ESPHome]({esphome_url}) node." } } } diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 330c2823409..8ac9feb8a93 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -9,7 +9,7 @@ "connection_error": "Impossible de se connecter \u00e0 ESP. Assurez-vous que votre fichier YAML contient une ligne 'api:'.", "invalid_auth": "Authentification non valide", "invalid_psk": "La cl\u00e9 de chiffrement de transport n\u2019est pas valide. Assurez-vous qu\u2019elle correspond \u00e0 ce que vous avez dans votre configuration", - "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, veuillez d\u00e9finir une adresse IP statique: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, essayez de d\u00e9finir une adresse IP statique" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "H\u00f4te", "port": "Port" }, - "description": "Veuillez saisir les param\u00e8tres de connexion de votre n\u0153ud [ESPHome] (https://esphomelib.com/)." + "description": "Veuillez saisir les param\u00e8tres de connexion de votre n\u0153ud [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index 51fd312fd6d..984358f02ba 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -10,7 +10,7 @@ "websocket_not_enabled": "Forked-Daapd-Server-Websocket nicht aktiviert.", "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", - "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich." + "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version >= 27.0 erforderlich." }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/lcn/translations/et.json b/homeassistant/components/lcn/translations/et.json index 058873e63c5..e390ed3a2f3 100644 --- a/homeassistant/components/lcn/translations/et.json +++ b/homeassistant/components/lcn/translations/et.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "koodluku kood vastu v\u00f5etud", "fingerprint": "vastu v\u00f5etud s\u00f5rmej\u00e4ljekood", "send_keys": "vastuv\u00f5etud v\u00f5tmete saatmine", "transmitter": "saatja kood vastu v\u00f5etud", diff --git a/homeassistant/components/lg_soundbar/translations/de.json b/homeassistant/components/lg_soundbar/translations/de.json new file mode 100644 index 00000000000..a840fb04abe --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/fr.json b/homeassistant/components/lg_soundbar/translations/fr.json new file mode 100644 index 00000000000..b13f3d0d595 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 67f014e0a2c..9e6e819a179 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -29,7 +29,7 @@ "username": "Benutzername" }, "description": "Erweiterte Optionen sind in der [Life360-Dokumentation]({docs_url}) zu finden.\nDies sollte vor dem Hinzuf\u00fcgen von Kontoinformationen getan werden.", - "title": "Life360-Kontoinformationen" + "title": "Life360-Konto konfigurieren" } } }, diff --git a/homeassistant/components/life360/translations/el.json b/homeassistant/components/life360/translations/el.json index 07106d89d63..f0db8e10ed6 100644 --- a/homeassistant/components/life360/translations/el.json +++ b/homeassistant/components/life360/translations/el.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "invalid_username": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", @@ -23,5 +32,19 @@ "title": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Life360" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03bf\u03b4\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c9\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", + "driving_speed": "\u03a4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 \u03bf\u03b4\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2", + "limit_gps_acc": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1\u03c2 GPS", + "max_gps_accuracy": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 GPS (\u03bc\u03ad\u03c4\u03c1\u03b1)", + "set_drive_speed": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03c1\u03b9\u03bf \u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bf\u03b4\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/et.json b/homeassistant/components/life360/translations/et.json index d9cbbbb30f5..c6e66af5d08 100644 --- a/homeassistant/components/life360/translations/et.json +++ b/homeassistant/components/life360/translations/et.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", "invalid_auth": "Tuvastamise viga", + "reauth_successful": "Taastuvastamine \u00f5nnestus", "unknown": "Ootamatu t\u00f5rge" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "Kasutaja on juba seadistatud", + "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamise viga", "invalid_username": "Vale kasutajanimi", "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", @@ -23,5 +32,19 @@ "title": "Life360 konto teave" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Kuva s\u00f5itmist olekuna", + "driving_speed": "S\u00f5idukiirus", + "limit_gps_acc": "GPS-i t\u00e4psuse piiramine", + "max_gps_accuracy": "GPS-i maksimaalne t\u00e4psus (meetrites)", + "set_drive_speed": "M\u00e4\u00e4ra s\u00f5idukiiruse l\u00e4vi" + }, + "title": "Konto suvandid" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index 772b44b31d8..4776e3409ea 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_username": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", @@ -23,5 +32,19 @@ "title": "Life360\u30a2\u30ab\u30a6\u30f3\u30c8\u60c5\u5831" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u30c9\u30e9\u30a4\u30d3\u30f3\u30b0\u3092\u72b6\u614b\u3068\u3057\u3066\u8868\u793a\u3059\u308b", + "driving_speed": "\u8d70\u884c\u901f\u5ea6", + "limit_gps_acc": "GPS\u306e\u7cbe\u5ea6\u3092\u5236\u9650\u3059\u308b", + "max_gps_accuracy": "GPS\u306e\u6700\u5927\u7cbe\u5ea6(\u30e1\u30fc\u30c8\u30eb)", + "set_drive_speed": "\u8d70\u884c\u901f\u5ea6\u306e\u3057\u304d\u3044\u5024\u3092\u8a2d\u5b9a\u3059\u308b" + }, + "title": "\u30a2\u30ab\u30a6\u30f3\u30c8\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index 612b0d5c4f7..cdcc937a33e 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Account is al geconfigureerd", "invalid_auth": "Ongeldige authenticatie", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "Account is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "invalid_username": "Ongeldige gebruikersnaam", "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "title": "Integratie herauthenticeren" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/life360/translations/no.json b/homeassistant/components/life360/translations/no.json index 9a95a976657..7213a665607 100644 --- a/homeassistant/components/life360/translations/no.json +++ b/homeassistant/components/life360/translations/no.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "invalid_auth": "Ugyldig godkjenning", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Kontoen er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "invalid_username": "Ugyldig brukernavn", "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", "username": "Brukernavn" }, "description": "For \u00e5 angi avanserte alternativer, se [Life360 dokumentasjon]({docs_url}). \nDet kan hende du vil gj\u00f8re det f\u00f8r du legger til kontoer.", - "title": "Life360 Kontoinformasjon" + "title": "Konfigurer Life360-konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Vis kj\u00f8ring som tilstand", + "driving_speed": "Kj\u00f8rehastighet", + "limit_gps_acc": "Begrens GPS-n\u00f8yaktigheten", + "max_gps_accuracy": "Maksimal GPS-n\u00f8yaktighet (meter)", + "set_drive_speed": "Still inn kj\u00f8rehastighetsterskel" + }, + "title": "Kontoalternativer" } } } diff --git a/homeassistant/components/mutesync/translations/de.json b/homeassistant/components/mutesync/translations/de.json index dccab9e8d1e..9ff12a5fad5 100644 --- a/homeassistant/components/mutesync/translations/de.json +++ b/homeassistant/components/mutesync/translations/de.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Aktiviere die Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", + "invalid_auth": "Aktiviere die Authentifizierung in den Einstellungen von m\u00fctesync \u2192 Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/nina/translations/et.json b/homeassistant/components/nina/translations/et.json index db454b7996c..4eb59ab43ae 100644 --- a/homeassistant/components/nina/translations/et.json +++ b/homeassistant/components/nina/translations/et.json @@ -23,5 +23,27 @@ "title": "Vali linn/maakond" } } + }, + "options": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "no_selection": "Vali v\u00e4hemalt \u00fcks linn/maakond", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Linn/maakond (A-D)", + "_e_to_h": "Linn/maakond (E-H)", + "_i_to_l": "Linn/maakond (I-L)", + "_m_to_q": "Linn/maakond (M-Q)", + "_r_to_u": "Linn/maakond (R-U)", + "_v_to_z": "Linn/maakond (V-Z)", + "corona_filter": "Eemalda koroonahoiatused", + "slots": "Maksimaalne hoiatuste arv linna/maakonna kohta" + }, + "title": "Valikud" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ja.json b/homeassistant/components/nina/translations/ja.json index 5e3978f4777..fa3fa6f4995 100644 --- a/homeassistant/components/nina/translations/ja.json +++ b/homeassistant/components/nina/translations/ja.json @@ -34,6 +34,7 @@ "init": { "data": { "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", "_i_to_l": "City/county (I-L)", "_m_to_q": "City/county (M-Q)", "_r_to_u": "City/county (R-U)", diff --git a/homeassistant/components/nina/translations/nl.json b/homeassistant/components/nina/translations/nl.json index 4b0100f9f07..1b407576bbd 100644 --- a/homeassistant/components/nina/translations/nl.json +++ b/homeassistant/components/nina/translations/nl.json @@ -23,5 +23,16 @@ "title": "Selecteer stad/provincie" } } + }, + "options": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "init": { + "title": "Opties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/no.json b/homeassistant/components/nina/translations/no.json index a4e062a7812..6a13c698941 100644 --- a/homeassistant/components/nina/translations/no.json +++ b/homeassistant/components/nina/translations/no.json @@ -23,5 +23,27 @@ "title": "Velg by/fylke" } } + }, + "options": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "no_selection": "Velg minst \u00e9n by/fylke", + "unknown": "Uventet feil" + }, + "step": { + "init": { + "data": { + "_a_to_d": "By/fylke (AD)", + "_e_to_h": "By/fylke (EH)", + "_i_to_l": "By/fylke (IL)", + "_m_to_q": "By/fylke (MQ)", + "_r_to_u": "By/fylke (RU)", + "_v_to_z": "By/fylke (VZ)", + "corona_filter": "Fjern koronaadvarsler", + "slots": "Maksimal advarsler per by/fylke" + }, + "title": "Alternativer" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index 46ca14c81ac..d02571a6a50 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "\n\nUnter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen -> Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links -> Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." + "default": "\n\nUnter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen \u2192 Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links \u2192 Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index 900bac61bc5..ee691ac3901 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/smartthings/translations/de.json b/homeassistant/components/smartthings/translations/de.json index 6cd7157b702..b6a97013784 100644 --- a/homeassistant/components/smartthings/translations/de.json +++ b/homeassistant/components/smartthings/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant ist nicht richtig konfiguriert, um Updates von SmartThings zu erhalten. Die Webhook-URL ist ung\u00fcltig: \n > {webhook_url} \n\nBitte aktualisiere deine Konfiguration gem\u00e4\u00df den [Anweisungen] ({component_url}), starte den Home Assistant neu und versuche es erneut.", + "invalid_webhook_url": "Home Assistant ist nicht richtig konfiguriert, um Updates von SmartThings zu erhalten. Die Webhook-URL ist ung\u00fcltig: \n \u2192 {webhook_url} \n\nBitte aktualisiere deine Konfiguration gem\u00e4\u00df den [Anweisungen] ({component_url}), starte den Home Assistant neu und versuche es erneut.", "no_available_locations": "In Home Assistant sind keine SmartThings-Standorte zum Einrichten verf\u00fcgbar." }, "error": { @@ -30,7 +30,7 @@ "title": "Standort ausw\u00e4hlen" }, "user": { - "description": "SmartThings wird so konfiguriert, dass Push-Updates an Home Assistant gesendet werden an die URL: \n > {webhook_url} \n\nWenn dies nicht korrekt ist, aktualisiere bitte deine Konfiguration, starte Home Assistant neu und versuche es erneut.", + "description": "SmartThings wird so konfiguriert, dass Push-Updates an Home Assistant gesendet werden an die URL: \n \u2192 {webhook_url} \n\nWenn dies nicht korrekt ist, aktualisiere bitte deine Konfiguration, starte Home Assistant neu und versuche es erneut.", "title": "R\u00fcckruf-URL best\u00e4tigen" } } diff --git a/homeassistant/components/starline/translations/de.json b/homeassistant/components/starline/translations/de.json index 87a9249475e..22e60dd10c0 100644 --- a/homeassistant/components/starline/translations/de.json +++ b/homeassistant/components/starline/translations/de.json @@ -26,7 +26,7 @@ "mfa_code": "SMS Code" }, "description": "Gib den an das Telefon gesendeten Code ein {Telefon_Nummer}", - "title": "2-Faktor-Authentifizierung" + "title": "Zwei-Faktor-Authentifizierung" }, "auth_user": { "data": { diff --git a/homeassistant/components/threshold/translations/sv.json b/homeassistant/components/threshold/translations/sv.json index 613b2c25412..d19b6cabb91 100644 --- a/homeassistant/components/threshold/translations/sv.json +++ b/homeassistant/components/threshold/translations/sv.json @@ -12,5 +12,6 @@ } } } - } + }, + "title": "Gr\u00e4nsv\u00e4rdessensor" } \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/de.json b/homeassistant/components/zwave_me/translations/de.json index 747ccf0c9c8..6e20c28ce07 100644 --- a/homeassistant/components/zwave_me/translations/de.json +++ b/homeassistant/components/zwave_me/translations/de.json @@ -13,7 +13,7 @@ "token": "API-Token", "url": "URL" }, - "description": "Gib die IP-Adresse mit Port und Zugangs-Token des Z-Way-Servers ein. Um das Token zu erhalten, gehe zur Z-Way-Benutzeroberfl\u00e4che Smart Home UI > Men\u00fc > Einstellungen > Benutzer > Administrator > API-Token.\n\nBeispiel f\u00fcr die Verbindung zu Z-Way im lokalen Netzwerk:\nURL: {local_url}\nToken: {local_token}\n\nBeispiel f\u00fcr die Verbindung zu Z-Way \u00fcber den Fernzugriff find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nBeispiel f\u00fcr eine Verbindung zu Z-Way mit einer statischen \u00f6ffentlichen IP-Adresse:\nURL: {remote_url}\nToken: {local_token}\n\nWenn du dich \u00fcber find.z-wave.me verbindest, musst du ein Token mit globalem Geltungsbereich verwenden (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." + "description": "Gib die IP-Adresse mit Port und Zugangs-Token des Z-Way-Servers ein. Um das Token zu erhalten, gehe zur Z-Way-Benutzeroberfl\u00e4che Smart Home UI \u2192 Men\u00fc \u2192 Einstellungen \u2192 Benutzer \u2192 Administrator \u2192 API-Token.\n\nBeispiel f\u00fcr die Verbindung zu Z-Way im lokalen Netzwerk:\nURL: {local_url}\nToken: {local_token}\n\nBeispiel f\u00fcr die Verbindung zu Z-Way \u00fcber den Fernzugriff find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nBeispiel f\u00fcr eine Verbindung zu Z-Way mit einer statischen \u00f6ffentlichen IP-Adresse:\nURL: {remote_url}\nToken: {local_token}\n\nWenn du dich \u00fcber find.z-wave.me verbindest, musst du ein Token mit globalem Geltungsbereich verwenden (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." } } } From 43595f7e17adb95db5a7287d583c40f00ba80fa1 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 1 Jul 2022 06:08:21 +0200 Subject: [PATCH 054/820] Add light tests for devolo_home_control (#74183) --- .coveragerc | 1 - tests/components/devolo_home_control/mocks.py | 41 +++++ .../devolo_home_control/test_light.py | 165 ++++++++++++++++++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 tests/components/devolo_home_control/test_light.py diff --git a/.coveragerc b/.coveragerc index f12d1ce88fa..21534333583 100644 --- a/.coveragerc +++ b/.coveragerc @@ -210,7 +210,6 @@ omit = homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py - homeassistant/components/devolo_home_control/light.py homeassistant/components/devolo_home_control/sensor.py homeassistant/components/devolo_home_control/switch.py homeassistant/components/digital_ocean/* diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index e9dae0b70b1..129c4a377ef 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -8,6 +8,9 @@ from devolo_home_control_api.homecontrol import HomeControl from devolo_home_control_api.properties.binary_sensor_property import ( BinarySensorProperty, ) +from devolo_home_control_api.properties.binary_switch_property import ( + BinarySwitchProperty, +) from devolo_home_control_api.properties.multi_level_sensor_property import ( MultiLevelSensorProperty, ) @@ -31,6 +34,15 @@ class BinarySensorPropertyMock(BinarySensorProperty): self.state = False +class BinarySwitchPropertyMock(BinarySwitchProperty): + """devolo Home Control binary sensor mock.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + self._logger = MagicMock() + self.element_uid = "Test" + + class MultiLevelSensorPropertyMock(MultiLevelSensorProperty): """devolo Home Control multi level sensor mock.""" @@ -134,6 +146,22 @@ class CoverMock(DeviceMock): } +class LightMock(DeviceMock): + """devolo Home Control light device mock.""" + + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.binary_switch_property = {} + self.multi_level_switch_property = { + "devolo.Dimmer:Test": MultiLevelSwitchPropertyMock() + } + self.multi_level_switch_property["devolo.Dimmer:Test"].switch_type = "dimmer" + self.multi_level_switch_property[ + "devolo.Dimmer:Test" + ].element_uid = "devolo.Dimmer:Test" + + class RemoteControlMock(DeviceMock): """devolo Home Control remote control device mock.""" @@ -219,6 +247,19 @@ class HomeControlMockCover(HomeControlMock): self.publisher.unregister = MagicMock() +class HomeControlMockLight(HomeControlMock): + """devolo Home Control gateway mock with light devices.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = { + "Test": LightMock(), + } + self.publisher = Publisher(self.devices.keys()) + self.publisher.unregister = MagicMock() + + class HomeControlMockRemoteControl(HomeControlMock): """devolo Home Control gateway mock with remote control device.""" diff --git a/tests/components/devolo_home_control/test_light.py b/tests/components/devolo_home_control/test_light.py new file mode 100644 index 00000000000..7b18b28a493 --- /dev/null +++ b/tests/components/devolo_home_control/test_light.py @@ -0,0 +1,165 @@ +"""Tests for the devolo Home Control light platform.""" +from unittest.mock import patch + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_SUPPORTED_COLOR_MODES, + DOMAIN, + ColorMode, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from . import configure_integration +from .mocks import BinarySwitchPropertyMock, HomeControlMock, HomeControlMockLight + + +async def test_light_without_binary_sensor(hass: HomeAssistant): + """Test setup and state change of a light device that does not have an additional binary sensor.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockLight() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] + assert state.attributes[ATTR_BRIGHTNESS] == round( + test_gateway.devices["Test"] + .multi_level_switch_property["devolo.Dimmer:Test"] + .value + / 100 + * 255 + ) + + # Emulate websocket message: brightness changed + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 0.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_OFF + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 100.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + + # Test setting brightness + with patch( + "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" + ) as set_value: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(100) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(0) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: f"{DOMAIN}.test", ATTR_BRIGHTNESS: 50}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(round(50 / 255 * 100)) + + # Emulate websocket message: device went offline + test_gateway.devices["Test"].status = 1 + test_gateway.publisher.dispatch("Test", ("Status", False, "status")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + + +async def test_light_with_binary_sensor(hass: HomeAssistant): + """Test setup and state change of a light device that has an additional binary sensor.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockLight() + test_gateway.devices["Test"].binary_switch_property = { + "devolo.BinarySwitch:Test": BinarySwitchPropertyMock() + } + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == STATE_ON + + # Emulate websocket message: brightness changed + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 0.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_OFF + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 100.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + + # Test setting brightness + with patch( + "devolo_home_control_api.properties.binary_switch_property.BinarySwitchProperty.set" + ) as set_value: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(True) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(False) + + +async def test_remove_from_hass(hass: HomeAssistant): + """Test removing entity.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockLight() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert test_gateway.publisher.unregister.call_count == 1 From 7655b84494f304fe48f0bb0fd93ab9cfb12c9ff8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 00:19:40 -0500 Subject: [PATCH 055/820] Fix key collision between platforms in esphome state updates (#74273) --- homeassistant/components/esphome/__init__.py | 19 +--- .../components/esphome/entry_data.py | 92 ++++++++++++++----- 2 files changed, 73 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2e88a883dc1..0c1eac3aa45 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -150,11 +150,6 @@ async def async_setup_entry( # noqa: C901 hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) - @callback - def async_on_state(state: EntityState) -> None: - """Send dispatcher updates when a new state is received.""" - entry_data.async_update_state(hass, state) - @callback def async_on_service_call(service: HomeassistantServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" @@ -288,7 +283,7 @@ async def async_setup_entry( # noqa: C901 entity_infos, services = await cli.list_entities_services() await entry_data.async_update_static_infos(hass, entry, entity_infos) await _setup_services(hass, entry_data, services) - await cli.subscribe_states(async_on_state) + await cli.subscribe_states(entry_data.async_update_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states(async_on_state_subscription) @@ -568,7 +563,6 @@ async def platform_async_setup_entry( @callback def async_list_entities(infos: list[EntityInfo]) -> None: """Update entities of this platform when entities are listed.""" - key_to_component = entry_data.key_to_component old_infos = entry_data.info[component_key] new_infos: dict[int, EntityInfo] = {} add_entities = [] @@ -587,12 +581,10 @@ async def platform_async_setup_entry( entity = entity_type(entry_data, component_key, info.key) add_entities.append(entity) new_infos[info.key] = info - key_to_component[info.key] = component_key # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) - key_to_component.pop(info.key, None) # First copy the now-old info into the backup object entry_data.old_info[component_key] = entry_data.info[component_key] @@ -714,13 +706,8 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): ) self.async_on_remove( - async_dispatcher_connect( - self.hass, - ( - f"esphome_{self._entry_id}" - f"_update_{self._component_key}_{self._key}" - ), - self._on_state_update, + self._entry_data.async_subscribe_state_update( + self._component_key, self._key, self._on_state_update ) ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d4bcc67db4a..8eb56e6fdb6 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -12,26 +12,40 @@ from aioesphomeapi import ( APIClient, APIVersion, BinarySensorInfo, + BinarySensorState, CameraInfo, + CameraState, ClimateInfo, + ClimateState, CoverInfo, + CoverState, DeviceInfo, EntityInfo, EntityState, FanInfo, + FanState, LightInfo, + LightState, LockInfo, + LockState, MediaPlayerInfo, + MediaPlayerState, NumberInfo, + NumberState, SelectInfo, + SelectState, SensorInfo, + SensorState, SwitchInfo, + SwitchState, TextSensorInfo, + TextSensorState, UserService, ) from aioesphomeapi.model import ButtonInfo from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store @@ -41,20 +55,37 @@ _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { - BinarySensorInfo: "binary_sensor", - ButtonInfo: "button", - CameraInfo: "camera", - ClimateInfo: "climate", - CoverInfo: "cover", - FanInfo: "fan", - LightInfo: "light", - LockInfo: "lock", - MediaPlayerInfo: "media_player", - NumberInfo: "number", - SelectInfo: "select", - SensorInfo: "sensor", - SwitchInfo: "switch", - TextSensorInfo: "sensor", + BinarySensorInfo: Platform.BINARY_SENSOR, + ButtonInfo: Platform.BINARY_SENSOR, + CameraInfo: Platform.BINARY_SENSOR, + ClimateInfo: Platform.CLIMATE, + CoverInfo: Platform.COVER, + FanInfo: Platform.FAN, + LightInfo: Platform.LIGHT, + LockInfo: Platform.LOCK, + MediaPlayerInfo: Platform.MEDIA_PLAYER, + NumberInfo: Platform.NUMBER, + SelectInfo: Platform.SELECT, + SensorInfo: Platform.SENSOR, + SwitchInfo: Platform.SWITCH, + TextSensorInfo: Platform.SENSOR, +} + +STATE_TYPE_TO_COMPONENT_KEY = { + BinarySensorState: Platform.BINARY_SENSOR, + EntityState: Platform.BINARY_SENSOR, + CameraState: Platform.BINARY_SENSOR, + ClimateState: Platform.CLIMATE, + CoverState: Platform.COVER, + FanState: Platform.FAN, + LightState: Platform.LIGHT, + LockState: Platform.LOCK, + MediaPlayerState: Platform.MEDIA_PLAYER, + NumberState: Platform.NUMBER, + SelectState: Platform.SELECT, + SensorState: Platform.SENSOR, + SwitchState: Platform.SWITCH, + TextSensorState: Platform.SENSOR, } @@ -67,7 +98,6 @@ class RuntimeEntryData: store: Store state: dict[str, dict[int, EntityState]] = field(default_factory=dict) info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) - key_to_component: dict[int, str] = field(default_factory=dict) # A second list of EntityInfo objects # This is necessary for when an entity is being removed. HA requires @@ -81,6 +111,9 @@ class RuntimeEntryData: api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) + state_subscriptions: dict[tuple[str, int], Callable[[], None]] = field( + default_factory=dict + ) loaded_platforms: set[str] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None @@ -125,18 +158,33 @@ class RuntimeEntryData: async_dispatcher_send(hass, signal, infos) @callback - def async_update_state(self, hass: HomeAssistant, state: EntityState) -> None: + def async_subscribe_state_update( + self, + component_key: str, + state_key: int, + entity_callback: Callable[[], None], + ) -> Callable[[], None]: + """Subscribe to state updates.""" + + def _unsubscribe() -> None: + self.state_subscriptions.pop((component_key, state_key)) + + self.state_subscriptions[(component_key, state_key)] = entity_callback + return _unsubscribe + + @callback + def async_update_state(self, state: EntityState) -> None: """Distribute an update of state information to the target.""" - component_key = self.key_to_component[state.key] + component_key = STATE_TYPE_TO_COMPONENT_KEY[type(state)] + subscription_key = (component_key, state.key) self.state[component_key][state.key] = state - signal = f"esphome_{self.entry_id}_update_{component_key}_{state.key}" _LOGGER.debug( - "Dispatching update for component %s with state key %s: %s", - component_key, - state.key, + "Dispatching update with key %s: %s", + subscription_key, state, ) - async_dispatcher_send(hass, signal) + if subscription_key in self.state_subscriptions: + self.state_subscriptions[subscription_key]() @callback def async_update_device_state(self, hass: HomeAssistant) -> None: From 273e9b287f482f5d1f0718ebf09a56c49c66513d Mon Sep 17 00:00:00 2001 From: Stefan Rado Date: Fri, 1 Jul 2022 07:20:00 +0200 Subject: [PATCH 056/820] Add config flow for Bose SoundTouch (#72967) Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 + .../components/discovery/__init__.py | 2 +- .../components/soundtouch/__init__.py | 141 +++++++++++ .../components/soundtouch/config_flow.py | 104 ++++++++ homeassistant/components/soundtouch/const.py | 2 +- .../components/soundtouch/manifest.json | 9 +- .../components/soundtouch/media_player.py | 238 ++++++------------ .../components/soundtouch/services.yaml | 8 +- .../components/soundtouch/strings.json | 21 ++ .../soundtouch/translations/en.json | 21 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 5 + tests/components/soundtouch/conftest.py | 34 ++- .../components/soundtouch/test_config_flow.py | 113 +++++++++ .../soundtouch/test_media_player.py | 119 ++++----- 15 files changed, 584 insertions(+), 236 deletions(-) create mode 100644 homeassistant/components/soundtouch/config_flow.py create mode 100644 homeassistant/components/soundtouch/strings.json create mode 100644 homeassistant/components/soundtouch/translations/en.json create mode 100644 tests/components/soundtouch/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index ed4ab888541..9845f5f7e5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -979,6 +979,8 @@ build.json @home-assistant/supervisor /tests/components/songpal/ @rytilahti @shenxn /homeassistant/components/sonos/ @cgtobi @jjlawren /tests/components/sonos/ @cgtobi @jjlawren +/homeassistant/components/soundtouch/ @kroimon +/tests/components/soundtouch/ @kroimon /homeassistant/components/spaceapi/ @fabaff /tests/components/spaceapi/ @fabaff /homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87 diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 3c3538c1ca0..cc104cc2110 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -61,7 +61,6 @@ SERVICE_HANDLERS = { "yamaha": ServiceDetails("media_player", "yamaha"), "frontier_silicon": ServiceDetails("media_player", "frontier_silicon"), "openhome": ServiceDetails("media_player", "openhome"), - "bose_soundtouch": ServiceDetails("media_player", "soundtouch"), "bluesound": ServiceDetails("media_player", "bluesound"), } @@ -70,6 +69,7 @@ OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {} MIGRATED_SERVICE_HANDLERS = [ SERVICE_APPLE_TV, "axis", + "bose_soundtouch", "deconz", SERVICE_DAIKIN, "denonavr", diff --git a/homeassistant/components/soundtouch/__init__.py b/homeassistant/components/soundtouch/__init__.py index 6cd3c88fefc..69e0eef687e 100644 --- a/homeassistant/components/soundtouch/__init__.py +++ b/homeassistant/components/soundtouch/__init__.py @@ -1 +1,142 @@ """The soundtouch component.""" +import logging + +from libsoundtouch import soundtouch_device +from libsoundtouch.device import SoundTouchDevice +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, Platform +from homeassistant.core import HomeAssistant, ServiceCall +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import ( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + SERVICE_CREATE_ZONE, + SERVICE_PLAY_EVERYWHERE, + SERVICE_REMOVE_ZONE_SLAVE, +) + +_LOGGER = logging.getLogger(__name__) + +SERVICE_PLAY_EVERYWHERE_SCHEMA = vol.Schema({vol.Required("master"): cv.entity_id}) +SERVICE_CREATE_ZONE_SCHEMA = vol.Schema( + { + vol.Required("master"): cv.entity_id, + vol.Required("slaves"): cv.entity_ids, + } +) +SERVICE_ADD_ZONE_SCHEMA = vol.Schema( + { + vol.Required("master"): cv.entity_id, + vol.Required("slaves"): cv.entity_ids, + } +) +SERVICE_REMOVE_ZONE_SCHEMA = vol.Schema( + { + vol.Required("master"): cv.entity_id, + vol.Required("slaves"): cv.entity_ids, + } +) + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +class SoundTouchData: + """SoundTouch data stored in the Home Assistant data object.""" + + def __init__(self, device: SoundTouchDevice) -> None: + """Initialize the SoundTouch data object for a device.""" + self.device = device + self.media_player = None + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Bose SoundTouch component.""" + + async def service_handle(service: ServiceCall) -> None: + """Handle the applying of a service.""" + master_id = service.data.get("master") + slaves_ids = service.data.get("slaves") + slaves = [] + if slaves_ids: + slaves = [ + data.media_player + for data in hass.data[DOMAIN].values() + if data.media_player.entity_id in slaves_ids + ] + + master = next( + iter( + [ + data.media_player + for data in hass.data[DOMAIN].values() + if data.media_player.entity_id == master_id + ] + ), + None, + ) + + if master is None: + _LOGGER.warning("Unable to find master with entity_id: %s", str(master_id)) + return + + if service.service == SERVICE_PLAY_EVERYWHERE: + slaves = [ + data.media_player + for data in hass.data[DOMAIN].values() + if data.media_player.entity_id != master_id + ] + await hass.async_add_executor_job(master.create_zone, slaves) + elif service.service == SERVICE_CREATE_ZONE: + await hass.async_add_executor_job(master.create_zone, slaves) + elif service.service == SERVICE_REMOVE_ZONE_SLAVE: + await hass.async_add_executor_job(master.remove_zone_slave, slaves) + elif service.service == SERVICE_ADD_ZONE_SLAVE: + await hass.async_add_executor_job(master.add_zone_slave, slaves) + + hass.services.async_register( + DOMAIN, + SERVICE_PLAY_EVERYWHERE, + service_handle, + schema=SERVICE_PLAY_EVERYWHERE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_CREATE_ZONE, + service_handle, + schema=SERVICE_CREATE_ZONE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_REMOVE_ZONE_SLAVE, + service_handle, + schema=SERVICE_REMOVE_ZONE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + service_handle, + schema=SERVICE_ADD_ZONE_SCHEMA, + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Bose SoundTouch from a config entry.""" + device = await hass.async_add_executor_job(soundtouch_device, entry.data[CONF_HOST]) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/soundtouch/config_flow.py b/homeassistant/components/soundtouch/config_flow.py new file mode 100644 index 00000000000..47b10912436 --- /dev/null +++ b/homeassistant/components/soundtouch/config_flow.py @@ -0,0 +1,104 @@ +"""Config flow for Bose SoundTouch integration.""" +import logging + +from libsoundtouch import soundtouch_device +from requests import RequestException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class SoundtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Bose SoundTouch.""" + + VERSION = 1 + + def __init__(self): + """Initialize a new SoundTouch config flow.""" + self.host = None + self.name = None + + async def async_step_import(self, import_data): + """Handle a flow initiated by configuration file.""" + self.host = import_data[CONF_HOST] + + try: + await self._async_get_device_id() + except RequestException: + return self.async_abort(reason="cannot_connect") + + return await self._async_create_soundtouch_entry() + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is not None: + self.host = user_input[CONF_HOST] + + try: + await self._async_get_device_id(raise_on_progress=False) + except RequestException: + errors["base"] = "cannot_connect" + else: + return await self._async_create_soundtouch_entry() + + return self.async_show_form( + step_id="user", + last_step=True, + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + } + ), + errors=errors, + ) + + async def async_step_zeroconf(self, discovery_info): + """Handle a flow initiated by a zeroconf discovery.""" + self.host = discovery_info.host + + try: + await self._async_get_device_id() + except RequestException: + return self.async_abort(reason="cannot_connect") + + self.context["title_placeholders"] = {"name": self.name} + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + return await self._async_create_soundtouch_entry() + return self.async_show_form( + step_id="zeroconf_confirm", + last_step=True, + description_placeholders={"name": self.name}, + ) + + async def _async_get_device_id(self, raise_on_progress: bool = True) -> None: + """Get device ID from SoundTouch device.""" + device = await self.hass.async_add_executor_job(soundtouch_device, self.host) + + # Check if already configured + await self.async_set_unique_id( + device.config.device_id, raise_on_progress=raise_on_progress + ) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) + + self.name = device.config.name + + async def _async_create_soundtouch_entry(self): + """Finish config flow and create a SoundTouch config entry.""" + return self.async_create_entry( + title=self.name, + data={ + CONF_HOST: self.host, + }, + ) diff --git a/homeassistant/components/soundtouch/const.py b/homeassistant/components/soundtouch/const.py index 37bf1d8cc2b..a6b2b3c9f5f 100644 --- a/homeassistant/components/soundtouch/const.py +++ b/homeassistant/components/soundtouch/const.py @@ -1,4 +1,4 @@ -"""Constants for the Bose Soundtouch component.""" +"""Constants for the Bose SoundTouch component.""" DOMAIN = "soundtouch" SERVICE_PLAY_EVERYWHERE = "play_everywhere" SERVICE_CREATE_ZONE = "create_zone" diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index 15091ec04f7..c1c2abd3b80 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -1,10 +1,11 @@ { "domain": "soundtouch", - "name": "Bose Soundtouch", + "name": "Bose SoundTouch", "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": ["libsoundtouch==0.8"], - "after_dependencies": ["zeroconf"], - "codeowners": [], + "zeroconf": ["_soundtouch._tcp.local."], + "codeowners": ["@kroimon"], "iot_class": "local_polling", - "loggers": ["libsoundtouch"] + "loggers": ["libsoundtouch"], + "config_flow": true } diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index f8a5191d9db..2ed3dd9beea 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -1,23 +1,25 @@ -"""Support for interface with a Bose Soundtouch.""" +"""Support for interface with a Bose SoundTouch.""" from __future__ import annotations from functools import partial import logging import re -from libsoundtouch import soundtouch_device +from libsoundtouch.device import SoundTouchDevice from libsoundtouch.utils import Source import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -27,19 +29,16 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ( - DOMAIN, - SERVICE_ADD_ZONE_SLAVE, - SERVICE_CREATE_ZONE, - SERVICE_PLAY_EVERYWHERE, - SERVICE_REMOVE_ZONE_SLAVE, -) +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -50,137 +49,57 @@ MAP_STATUS = { "STOP_STATE": STATE_OFF, } -DATA_SOUNDTOUCH = "soundtouch" ATTR_SOUNDTOUCH_GROUP = "soundtouch_group" ATTR_SOUNDTOUCH_ZONE = "soundtouch_zone" -SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({vol.Required("master"): cv.entity_id}) - -SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema( - {vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids} -) - -SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema( - {vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids} -) - -SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema( - {vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids} -) - -DEFAULT_NAME = "Bose Soundtouch" -DEFAULT_PORT = 8090 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_NAME, default=""): cv.string, + } + ), ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Bose Soundtouch platform.""" - if DATA_SOUNDTOUCH not in hass.data: - hass.data[DATA_SOUNDTOUCH] = [] - - if discovery_info: - host = discovery_info["host"] - port = int(discovery_info["port"]) - - # if device already exists by config - if host in [device.config["host"] for device in hass.data[DATA_SOUNDTOUCH]]: - return - - remote_config = {"id": "ha.component.soundtouch", "host": host, "port": port} - bose_soundtouch_entity = SoundTouchDevice(None, remote_config) - hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) - add_entities([bose_soundtouch_entity], True) - else: - name = config.get(CONF_NAME) - remote_config = { - "id": "ha.component.soundtouch", - "port": config.get(CONF_PORT), - "host": config.get(CONF_HOST), - } - bose_soundtouch_entity = SoundTouchDevice(name, remote_config) - hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) - add_entities([bose_soundtouch_entity], True) - - def service_handle(service: ServiceCall) -> None: - """Handle the applying of a service.""" - master_device_id = service.data.get("master") - slaves_ids = service.data.get("slaves") - slaves = [] - if slaves_ids: - slaves = [ - device - for device in hass.data[DATA_SOUNDTOUCH] - if device.entity_id in slaves_ids - ] - - master = next( - iter( - [ - device - for device in hass.data[DATA_SOUNDTOUCH] - if device.entity_id == master_device_id - ] - ), - None, + """Set up the Bose SoundTouch platform.""" + _LOGGER.warning( + "Configuration of the Bose SoundTouch platform in YAML is deprecated and will be " + "removed in a future release; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - - if master is None: - _LOGGER.warning( - "Unable to find master with entity_id: %s", str(master_device_id) - ) - return - - if service.service == SERVICE_PLAY_EVERYWHERE: - slaves = [ - d for d in hass.data[DATA_SOUNDTOUCH] if d.entity_id != master_device_id - ] - master.create_zone(slaves) - elif service.service == SERVICE_CREATE_ZONE: - master.create_zone(slaves) - elif service.service == SERVICE_REMOVE_ZONE_SLAVE: - master.remove_zone_slave(slaves) - elif service.service == SERVICE_ADD_ZONE_SLAVE: - master.add_zone_slave(slaves) - - hass.services.register( - DOMAIN, - SERVICE_PLAY_EVERYWHERE, - service_handle, - schema=SOUNDTOUCH_PLAY_EVERYWHERE, - ) - hass.services.register( - DOMAIN, - SERVICE_CREATE_ZONE, - service_handle, - schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SERVICE_REMOVE_ZONE_SLAVE, - service_handle, - schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SERVICE_ADD_ZONE_SLAVE, - service_handle, - schema=SOUNDTOUCH_ADD_ZONE_SCHEMA, ) -class SoundTouchDevice(MediaPlayerEntity): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Bose SoundTouch media player based on a config entry.""" + device = hass.data[DOMAIN][entry.entry_id].device + media_player = SoundTouchMediaPlayer(device) + + async_add_entities([media_player], True) + + hass.data[DOMAIN][entry.entry_id].media_player = media_player + + +class SoundTouchMediaPlayer(MediaPlayerEntity): """Representation of a SoundTouch Bose device.""" _attr_supported_features = ( @@ -197,28 +116,32 @@ class SoundTouchDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.BROWSE_MEDIA ) + _attr_device_class = MediaPlayerDeviceClass.SPEAKER - def __init__(self, name, config): - """Create Soundtouch Entity.""" + def __init__(self, device: SoundTouchDevice) -> None: + """Create SoundTouch media player entity.""" + + self._device = device + + self._attr_unique_id = self._device.config.device_id + self._attr_name = self._device.config.name + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device.config.device_id)}, + connections={ + (CONNECTION_NETWORK_MAC, format_mac(self._device.config.mac_address)) + }, + manufacturer="Bose Corporation", + model=self._device.config.type, + name=self._device.config.name, + ) - self._device = soundtouch_device(config["host"], config["port"]) - if name is None: - self._name = self._device.config.name - else: - self._name = name self._status = None self._volume = None - self._config = config self._zone = None - @property - def config(self): - """Return specific soundtouch configuration.""" - return self._config - @property def device(self): - """Return Soundtouch device.""" + """Return SoundTouch device.""" return self._device def update(self): @@ -232,17 +155,15 @@ class SoundTouchDevice(MediaPlayerEntity): """Volume level of the media player (0..1).""" return self._volume.actual / 100 - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def state(self): """Return the state of the device.""" - if self._status.source == "STANDBY": + if self._status is None or self._status.source == "STANDBY": return STATE_OFF + if self._status.source == "INVALID_SOURCE": + return STATE_UNKNOWN + return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE) @property @@ -478,15 +399,12 @@ class SoundTouchDevice(MediaPlayerEntity): if not zone_status: return None - # Due to a bug in the SoundTouch API itself client devices do NOT return their - # siblings as part of the "slaves" list. Only the master has the full list of - # slaves for some reason. To compensate for this shortcoming we have to fetch - # the zone info from the master when the current device is a slave until this is - # fixed in the SoundTouch API or libsoundtouch, or of course until somebody has a - # better idea on how to fix this. + # Client devices do NOT return their siblings as part of the "slaves" list. + # Only the master has the full list of slaves. To compensate for this shortcoming + # we have to fetch the zone info from the master when the current device is a slave. # In addition to this shortcoming, libsoundtouch seems to report the "is_master" # property wrong on some slaves, so the only reliable way to detect if the current - # devices is the master, is by comparing the master_id of the zone with the device_id + # devices is the master, is by comparing the master_id of the zone with the device_id. if zone_status.master_id == self._device.config.device_id: return self._build_zone_info(self.entity_id, zone_status.slaves) @@ -505,16 +423,16 @@ class SoundTouchDevice(MediaPlayerEntity): def _get_instance_by_ip(self, ip_address): """Search and return a SoundTouchDevice instance by it's IP address.""" - for instance in self.hass.data[DATA_SOUNDTOUCH]: - if instance and instance.config["host"] == ip_address: - return instance + for data in self.hass.data[DOMAIN].values(): + if data.device.config.device_ip == ip_address: + return data.media_player return None def _get_instance_by_id(self, instance_id): """Search and return a SoundTouchDevice instance by it's ID (aka MAC address).""" - for instance in self.hass.data[DATA_SOUNDTOUCH]: - if instance and instance.device.config.device_id == instance_id: - return instance + for data in self.hass.data[DOMAIN].values(): + if data.device.config.device_id == instance_id: + return data.media_player return None def _build_zone_info(self, master, zone_slaves): diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml index 8d255e5f069..82709053496 100644 --- a/homeassistant/components/soundtouch/services.yaml +++ b/homeassistant/components/soundtouch/services.yaml @@ -1,6 +1,6 @@ play_everywhere: name: Play everywhere - description: Play on all Bose Soundtouch devices. + description: Play on all Bose SoundTouch devices. fields: master: name: Master @@ -13,7 +13,7 @@ play_everywhere: create_zone: name: Create zone - description: Create a Soundtouch multi-room zone. + description: Create a SoundTouch multi-room zone. fields: master: name: Master @@ -35,7 +35,7 @@ create_zone: add_zone_slave: name: Add zone slave - description: Add a slave to a Soundtouch multi-room zone. + description: Add a slave to a SoundTouch multi-room zone. fields: master: name: Master @@ -57,7 +57,7 @@ add_zone_slave: remove_zone_slave: name: Remove zone slave - description: Remove a slave from the Soundtouch multi-room zone. + description: Remove a slave from the SoundTouch multi-room zone. fields: master: name: Master diff --git a/homeassistant/components/soundtouch/strings.json b/homeassistant/components/soundtouch/strings.json new file mode 100644 index 00000000000..7ebcd4c5285 --- /dev/null +++ b/homeassistant/components/soundtouch/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "zeroconf_confirm": { + "title": "Confirm adding Bose SoundTouch device", + "description": "You are about to add the SoundTouch device named `{name}` to Home Assistant." + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/soundtouch/translations/en.json b/homeassistant/components/soundtouch/translations/en.json new file mode 100644 index 00000000000..2e025d3f187 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "You are about to add the SoundTouch device named `{name}` to Home Assistant.", + "title": "Confirm adding Bose SoundTouch device" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b0da8f79418..0b985f6e161 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -334,6 +334,7 @@ FLOWS = { "sonarr", "songpal", "sonos", + "soundtouch", "speedtestdotnet", "spider", "spotify", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index faca1c17854..3c9d21d1d95 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -347,6 +347,11 @@ ZEROCONF = { "domain": "sonos" } ], + "_soundtouch._tcp.local.": [ + { + "domain": "soundtouch" + } + ], "_spotify-connect._tcp.local.": [ { "domain": "spotify" diff --git a/tests/components/soundtouch/conftest.py b/tests/components/soundtouch/conftest.py index dcac360d253..21de9e2ed47 100644 --- a/tests/components/soundtouch/conftest.py +++ b/tests/components/soundtouch/conftest.py @@ -4,9 +4,9 @@ from requests_mock import Mocker from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.soundtouch.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PLATFORM +from homeassistant.const import CONF_HOST, CONF_NAME -from tests.common import load_fixture +from tests.common import MockConfigEntry, load_fixture DEVICE_1_ID = "020000000001" DEVICE_2_ID = "020000000002" @@ -14,8 +14,8 @@ DEVICE_1_IP = "192.168.42.1" DEVICE_2_IP = "192.168.42.2" DEVICE_1_URL = f"http://{DEVICE_1_IP}:8090" DEVICE_2_URL = f"http://{DEVICE_2_IP}:8090" -DEVICE_1_NAME = "My Soundtouch 1" -DEVICE_2_NAME = "My Soundtouch 2" +DEVICE_1_NAME = "My SoundTouch 1" +DEVICE_2_NAME = "My SoundTouch 2" DEVICE_1_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_1" DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2" @@ -24,15 +24,29 @@ DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2" @pytest.fixture -def device1_config() -> dict[str, str]: - """Mock SoundTouch device 1 config.""" - yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_1_IP, CONF_NAME: DEVICE_1_NAME} +def device1_config() -> MockConfigEntry: + """Mock SoundTouch device 1 config entry.""" + yield MockConfigEntry( + domain=DOMAIN, + unique_id=DEVICE_1_ID, + data={ + CONF_HOST: DEVICE_1_IP, + CONF_NAME: "", + }, + ) @pytest.fixture -def device2_config() -> dict[str, str]: - """Mock SoundTouch device 2 config.""" - yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_2_IP, CONF_NAME: DEVICE_2_NAME} +def device2_config() -> MockConfigEntry: + """Mock SoundTouch device 2 config entry.""" + yield MockConfigEntry( + domain=DOMAIN, + unique_id=DEVICE_2_ID, + data={ + CONF_HOST: DEVICE_2_IP, + CONF_NAME: "", + }, + ) @pytest.fixture(scope="session") diff --git a/tests/components/soundtouch/test_config_flow.py b/tests/components/soundtouch/test_config_flow.py new file mode 100644 index 00000000000..cbeb27be979 --- /dev/null +++ b/tests/components/soundtouch/test_config_flow.py @@ -0,0 +1,113 @@ +"""Test config flow.""" +from unittest.mock import patch + +from requests import RequestException +from requests_mock import ANY, Mocker + +from homeassistant.components.soundtouch.const import DOMAIN +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import DEVICE_1_ID, DEVICE_1_IP, DEVICE_1_NAME + + +async def test_user_flow_create_entry( + hass: HomeAssistant, device1_requests_mock_standby: Mocker +) -> None: + """Test the full manual user flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + assert "flow_id" in result + + with patch( + "homeassistant.components.soundtouch.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: DEVICE_1_IP, + }, + ) + + assert len(mock_setup_entry.mock_calls) == 1 + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == DEVICE_1_NAME + assert result.get("data") == { + CONF_HOST: DEVICE_1_IP, + } + assert "result" in result + assert result["result"].unique_id == DEVICE_1_ID + assert result["result"].title == DEVICE_1_NAME + + +async def test_user_flow_cannot_connect( + hass: HomeAssistant, requests_mock: Mocker +) -> None: + """Test a manual user flow with an invalid host.""" + requests_mock.get(ANY, exc=RequestException()) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={ + CONF_HOST: "invalid-hostname", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_zeroconf_flow_create_entry( + hass: HomeAssistant, device1_requests_mock_standby: Mocker +) -> None: + """Test the zeroconf flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + host=DEVICE_1_IP, + addresses=[DEVICE_1_IP], + port=8090, + hostname="Bose-SM2-060000000001.local.", + type="_soundtouch._tcp.local.", + name=f"{DEVICE_1_NAME}._soundtouch._tcp.local.", + properties={ + "DESCRIPTION": "SoundTouch", + "MAC": DEVICE_1_ID, + "MANUFACTURER": "Bose Corporation", + "MODEL": "SoundTouch", + }, + ), + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "zeroconf_confirm" + assert result.get("description_placeholders") == {"name": DEVICE_1_NAME} + + with patch( + "homeassistant.components.soundtouch.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert len(mock_setup_entry.mock_calls) == 1 + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == DEVICE_1_NAME + assert result.get("data") == { + CONF_HOST: DEVICE_1_IP, + } + assert "result" in result + assert result["result"].unique_id == DEVICE_1_ID + assert result["result"].title == DEVICE_1_NAME diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 1b16508bb88..5105d07479c 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -1,4 +1,5 @@ """Test the SoundTouch component.""" +from datetime import timedelta from typing import Any from requests_mock import Mocker @@ -25,22 +26,26 @@ from homeassistant.components.soundtouch.const import ( from homeassistant.components.soundtouch.media_player import ( ATTR_SOUNDTOUCH_GROUP, ATTR_SOUNDTOUCH_ZONE, - DATA_SOUNDTOUCH, ) +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt from .conftest import DEVICE_1_ENTITY_ID, DEVICE_2_ENTITY_ID +from tests.common import MockConfigEntry, async_fire_time_changed -async def setup_soundtouch(hass: HomeAssistant, *configs: dict[str, str]): + +async def setup_soundtouch(hass: HomeAssistant, *mock_entries: MockConfigEntry): """Initialize media_player for tests.""" - assert await async_setup_component( - hass, MEDIA_PLAYER_DOMAIN, {MEDIA_PLAYER_DOMAIN: list(configs)} - ) + assert await async_setup_component(hass, MEDIA_PLAYER_DOMAIN, {}) + + for mock_entry in mock_entries: + mock_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - await hass.async_start() async def _test_key_service( @@ -59,7 +64,7 @@ async def _test_key_service( async def test_playing_media( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, ): """Test playing media info.""" @@ -76,7 +81,7 @@ async def test_playing_media( async def test_playing_radio( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_radio, ): """Test playing radio info.""" @@ -89,7 +94,7 @@ async def test_playing_radio( async def test_playing_aux( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_aux, ): """Test playing AUX info.""" @@ -102,7 +107,7 @@ async def test_playing_aux( async def test_playing_bluetooth( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_bluetooth, ): """Test playing Bluetooth info.""" @@ -118,7 +123,7 @@ async def test_playing_bluetooth( async def test_get_volume_level( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, ): """Test volume level.""" @@ -130,7 +135,7 @@ async def test_get_volume_level( async def test_get_state_off( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, ): """Test state device is off.""" @@ -142,7 +147,7 @@ async def test_get_state_off( async def test_get_state_pause( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp_paused, ): """Test state device is paused.""" @@ -154,7 +159,7 @@ async def test_get_state_pause( async def test_is_muted( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_volume_muted: str, ): @@ -170,7 +175,7 @@ async def test_is_muted( async def test_should_turn_off( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -187,7 +192,7 @@ async def test_should_turn_off( async def test_should_turn_on( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_key, ): @@ -204,7 +209,7 @@ async def test_should_turn_on( async def test_volume_up( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -221,7 +226,7 @@ async def test_volume_up( async def test_volume_down( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -238,7 +243,7 @@ async def test_volume_down( async def test_set_volume_level( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_volume, ): @@ -258,7 +263,7 @@ async def test_set_volume_level( async def test_mute( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -275,7 +280,7 @@ async def test_mute( async def test_play( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp_paused, device1_requests_mock_key, ): @@ -292,7 +297,7 @@ async def test_play( async def test_pause( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -309,7 +314,7 @@ async def test_pause( async def test_play_pause( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -326,7 +331,7 @@ async def test_play_pause( async def test_next_previous_track( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -351,7 +356,7 @@ async def test_next_previous_track( async def test_play_media( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -391,7 +396,7 @@ async def test_play_media( async def test_play_media_url( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_dlna, ): @@ -415,7 +420,7 @@ async def test_play_media_url( async def test_select_source_aux( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -435,7 +440,7 @@ async def test_select_source_aux( async def test_select_source_bluetooth( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -455,7 +460,7 @@ async def test_select_source_bluetooth( async def test_select_source_invalid_source( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -477,14 +482,25 @@ async def test_select_source_invalid_source( async def test_play_everywhere( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_set_zone, ): """Test play everywhere.""" - await setup_soundtouch(hass, device1_config, device2_config) + await setup_soundtouch(hass, device1_config) + + # no slaves, set zone must not be called + await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_EVERYWHERE, + {"master": DEVICE_1_ENTITY_ID}, + True, + ) + assert device1_requests_mock_set_zone.call_count == 0 + + await setup_soundtouch(hass, device2_config) # one master, one slave => set zone await hass.services.async_call( @@ -504,27 +520,11 @@ async def test_play_everywhere( ) assert device1_requests_mock_set_zone.call_count == 1 - # remove second device - for entity in list(hass.data[DATA_SOUNDTOUCH]): - if entity.entity_id == DEVICE_1_ENTITY_ID: - continue - hass.data[DATA_SOUNDTOUCH].remove(entity) - await entity.async_remove() - - # no slaves, set zone must not be called - await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_EVERYWHERE, - {"master": DEVICE_1_ENTITY_ID}, - True, - ) - assert device1_requests_mock_set_zone.call_count == 1 - async def test_create_zone( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_set_zone, @@ -567,8 +567,8 @@ async def test_create_zone( async def test_remove_zone_slave( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_remove_zone_slave, @@ -609,8 +609,8 @@ async def test_remove_zone_slave( async def test_add_zone_slave( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_add_zone_slave, @@ -651,14 +651,21 @@ async def test_add_zone_slave( async def test_zone_attributes( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, ): """Test zone attributes.""" await setup_soundtouch(hass, device1_config, device2_config) + # Fast-forward time to allow all entities to be set up and updated again + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] assert ( From 57b63db5677ece177b94dc27ffdb29c945564332 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 01:23:00 -0500 Subject: [PATCH 057/820] Improve typing for device_automation (#74282) --- .../components/device_automation/__init__.py | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 0a1ec495e70..93119d1b4a0 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable, Mapping +from collections.abc import Awaitable, Callable, Coroutine, Iterable, Mapping from enum import Enum from functools import wraps import logging @@ -13,6 +13,7 @@ import voluptuous as vol import voluptuous_serialize from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -43,10 +44,9 @@ if TYPE_CHECKING: DeviceAutomationActionProtocol, ] -# mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = "device_automation" -DEVICE_TRIGGER_BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( +DEVICE_TRIGGER_BASE_SCHEMA: vol.Schema = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str, @@ -310,11 +310,17 @@ async def _async_get_device_automation_capabilities( return capabilities # type: ignore[no-any-return] -def handle_device_errors(func): +def handle_device_errors( + func: Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Awaitable[None]] +) -> Callable[ + [HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None] +]: """Handle device automation errors.""" @wraps(func) - async def with_error_handling(hass, connection, msg): + async def with_error_handling( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] + ) -> None: try: await func(hass, connection, msg) except DeviceNotFound: @@ -333,7 +339,9 @@ def handle_device_errors(func): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_list_actions(hass, connection, msg): +async def websocket_device_automation_list_actions( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device actions.""" device_id = msg["device_id"] actions = ( @@ -352,7 +360,9 @@ async def websocket_device_automation_list_actions(hass, connection, msg): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_list_conditions(hass, connection, msg): +async def websocket_device_automation_list_conditions( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device conditions.""" device_id = msg["device_id"] conditions = ( @@ -371,7 +381,9 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_list_triggers(hass, connection, msg): +async def websocket_device_automation_list_triggers( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device triggers.""" device_id = msg["device_id"] triggers = ( @@ -390,7 +402,9 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_get_action_capabilities(hass, connection, msg): +async def websocket_device_automation_get_action_capabilities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device action capabilities.""" action = msg["action"] capabilities = await _async_get_device_automation_capabilities( @@ -409,7 +423,9 @@ async def websocket_device_automation_get_action_capabilities(hass, connection, ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_get_condition_capabilities(hass, connection, msg): +async def websocket_device_automation_get_condition_capabilities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device condition capabilities.""" condition = msg["condition"] capabilities = await _async_get_device_automation_capabilities( @@ -428,7 +444,9 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg): +async def websocket_device_automation_get_trigger_capabilities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device trigger capabilities.""" trigger = msg["trigger"] capabilities = await _async_get_device_automation_capabilities( From 7094b7e62f617257f0855d8f83ed9fbb4f36467a Mon Sep 17 00:00:00 2001 From: BrianWithAHat <19786223+BrianWithAHat@users.noreply.github.com> Date: Fri, 1 Jul 2022 03:17:27 -0400 Subject: [PATCH 058/820] Bump quantum_gateway to v0.0.8. (#74284) --- homeassistant/components/quantum_gateway/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index 5ce387dc9f3..49f674fdec6 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -2,7 +2,7 @@ "domain": "quantum_gateway", "name": "Quantum Gateway", "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", - "requirements": ["quantum-gateway==0.0.6"], + "requirements": ["quantum-gateway==0.0.8"], "codeowners": ["@cisasteelersfan"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0cb6d5122e1..75c6552de30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2047,7 +2047,7 @@ pyzerproc==0.4.8 qnapstats==0.4.0 # homeassistant.components.quantum_gateway -quantum-gateway==0.0.6 +quantum-gateway==0.0.8 # homeassistant.components.rachio rachiopy==1.0.3 From c78c159d722669a642ce89f631f46e4a9390b10c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 00:40:05 -0700 Subject: [PATCH 059/820] Add scan interval to scrape sensor (#74285) --- homeassistant/components/scrape/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index a73dbc17c1c..b6b8828ca73 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -44,6 +45,7 @@ from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DO _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=10) ICON = "mdi:web" PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( From a58301a97d7f90a791643607eff0ed74d5f59eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 1 Jul 2022 09:48:59 +0200 Subject: [PATCH 060/820] Improve qnap_qsw firmware coordinator failures (#74288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qnap_qsw: update: improve firmware coordinator failures Address late comments from @MartinHjelmare (MartinHjelmare). Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/__init__.py | 10 +++++++- .../components/qnap_qsw/coordinator.py | 4 +-- tests/components/qnap_qsw/test_init.py | 25 +++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index 4d2ee76cd2b..040f8eb52f2 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -1,16 +1,21 @@ """The QNAP QSW integration.""" from __future__ import annotations +import logging + from aioqsw.localapi import ConnectionOptions, QnapQswApi from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from .const import DOMAIN, QSW_COORD_DATA, QSW_COORD_FW from .coordinator import QswDataCoordinator, QswFirmwareCoordinator +_LOGGER = logging.getLogger(__name__) + PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.BUTTON, @@ -33,7 +38,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coord_data.async_config_entry_first_refresh() coord_fw = QswFirmwareCoordinator(hass, qsw) - await coord_fw.async_config_entry_first_refresh() + try: + await coord_fw.async_config_entry_first_refresh() + except ConfigEntryNotReady as error: + _LOGGER.warning(error) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { QSW_COORD_DATA: coord_data, diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index 73af2f74cc5..eb4e60bf9bd 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -5,7 +5,7 @@ from datetime import timedelta import logging from typing import Any -from aioqsw.exceptions import APIError, QswError +from aioqsw.exceptions import QswError from aioqsw.localapi import QnapQswApi import async_timeout @@ -63,8 +63,6 @@ class QswFirmwareCoordinator(DataUpdateCoordinator[dict[str, Any]]): async with async_timeout.timeout(QSW_TIMEOUT_SEC): try: await self.qsw.check_firmware() - except APIError as error: - _LOGGER.warning(error) except QswError as error: raise UpdateFailed(error) from error return self.qsw.data() diff --git a/tests/components/qnap_qsw/test_init.py b/tests/components/qnap_qsw/test_init.py index 75fa9ec8adc..fedfdd26543 100644 --- a/tests/components/qnap_qsw/test_init.py +++ b/tests/components/qnap_qsw/test_init.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from aioqsw.exceptions import APIError + from homeassistant.components.qnap_qsw.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -11,6 +13,29 @@ from .util import CONFIG from tests.common import MockConfigEntry +async def test_firmware_check_error(hass: HomeAssistant) -> None: + """Test firmware update check error.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="qsw_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.check_firmware", + side_effect=APIError, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.validate", + return_value=None, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.update", + return_value=None, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + async def test_unload_entry(hass: HomeAssistant) -> None: """Test unload.""" From c0ea1a38a6b42763d945a47d0a5a8894e07142f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 1 Jul 2022 11:52:46 +0200 Subject: [PATCH 061/820] Fix QNAP QSW DHCP discover bugs (#74291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qnqp_qsw: fix DHCP discover bugs Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/config_flow.py | 3 ++- homeassistant/components/qnap_qsw/strings.json | 6 ++++++ homeassistant/components/qnap_qsw/translations/en.json | 6 ++++++ tests/components/qnap_qsw/test_config_flow.py | 5 +++-- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index e9d11433021..bb42c9ea294 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -113,9 +113,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except LoginError: errors[CONF_PASSWORD] = "invalid_auth" except QswError: - errors[CONF_URL] = "cannot_connect" + errors["base"] = "cannot_connect" else: title = f"QNAP {system_board.get_product()} {self._discovered_mac}" + user_input[CONF_URL] = self._discovered_url return self.async_create_entry(title=title, data=user_input) return self.async_show_form( diff --git a/homeassistant/components/qnap_qsw/strings.json b/homeassistant/components/qnap_qsw/strings.json index 351245a9591..ba0cb28ba77 100644 --- a/homeassistant/components/qnap_qsw/strings.json +++ b/homeassistant/components/qnap_qsw/strings.json @@ -9,6 +9,12 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "step": { + "discovered_connection": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "data": { "url": "[%key:common::config_flow::data::url%]", diff --git a/homeassistant/components/qnap_qsw/translations/en.json b/homeassistant/components/qnap_qsw/translations/en.json index b6f68f2f062..c75c2d76ac8 100644 --- a/homeassistant/components/qnap_qsw/translations/en.json +++ b/homeassistant/components/qnap_qsw/translations/en.json @@ -9,6 +9,12 @@ "invalid_auth": "Invalid authentication" }, "step": { + "discovered_connection": { + "data": { + "password": "Password", + "username": "Username" + } + }, "user": { "data": { "password": "Password", diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 0b7072dd602..02f873c6a4a 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -24,7 +24,7 @@ DHCP_SERVICE_INFO = dhcp.DhcpServiceInfo( ) TEST_PASSWORD = "test-password" -TEST_URL = "test-url" +TEST_URL = f"http://{DHCP_SERVICE_INFO.ip}" TEST_USERNAME = "test-username" @@ -187,6 +187,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: assert result2["data"] == { CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, + CONF_URL: TEST_URL, } assert len(mock_setup_entry.mock_calls) == 1 @@ -237,7 +238,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant): }, ) - assert result["errors"] == {CONF_URL: "cannot_connect"} + assert result["errors"] == {"base": "cannot_connect"} async def test_dhcp_login_error(hass: HomeAssistant): From 72917f1d2c3a0a476b58f0004827530b0d62171d Mon Sep 17 00:00:00 2001 From: danaues Date: Fri, 1 Jul 2022 11:39:00 -0400 Subject: [PATCH 062/820] Lutron caseta ra3keypads (#74217) Co-authored-by: J. Nick Koston --- .../components/lutron_caseta/__init__.py | 31 +++-- .../lutron_caseta/device_trigger.py | 112 ++++++++++++------ .../lutron_caseta/test_device_trigger.py | 81 +++++++++++-- 3 files changed, 163 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index b4ce82a36c6..f2225900aad 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -250,6 +250,18 @@ def _area_and_name_from_name(device_name: str) -> tuple[str, str]: return UNASSIGNED_AREA, device_name +@callback +def async_get_lip_button(device_type: str, leap_button: int) -> int | None: + """Get the LIP button for a given LEAP button.""" + if ( + lip_buttons_name_to_num := DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device_type) + ) is None or ( + leap_button_num_to_name := LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get(device_type) + ) is None: + return None + return lip_buttons_name_to_num[leap_button_num_to_name[leap_button]] + + @callback def _async_subscribe_pico_remote_events( hass: HomeAssistant, @@ -271,21 +283,8 @@ def _async_subscribe_pico_remote_events( type_ = device["type"] area, name = _area_and_name_from_name(device["name"]) - button_number = device["button_number"] - # The original implementation used LIP instead of LEAP - # so we need to convert the button number to maintain compat - sub_type_to_lip_button = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP[type_] - leap_button_to_sub_type = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP[type_] - if (sub_type := leap_button_to_sub_type.get(button_number)) is None: - _LOGGER.error( - "Unknown LEAP button number %s is not in %s for %s (%s)", - button_number, - leap_button_to_sub_type, - name, - type_, - ) - return - lip_button_number = sub_type_to_lip_button[sub_type] + leap_button_number = device["button_number"] + lip_button_number = async_get_lip_button(type_, leap_button_number) hass_device = dev_reg.async_get_device({(DOMAIN, device["serial"])}) hass.bus.async_fire( @@ -294,7 +293,7 @@ def _async_subscribe_pico_remote_events( ATTR_SERIAL: device["serial"], ATTR_TYPE: type_, ATTR_BUTTON_NUMBER: lip_button_number, - ATTR_LEAP_BUTTON_NUMBER: button_number, + ATTR_LEAP_BUTTON_NUMBER: leap_button_number, ATTR_DEVICE_NAME: name, ATTR_DEVICE_ID: hass_device.id, ATTR_AREA_NAME: area, diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index e762e79a8d7..dcdf5d584a4 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -27,7 +27,7 @@ from .const import ( ACTION_PRESS, ACTION_RELEASE, ATTR_ACTION, - ATTR_BUTTON_NUMBER, + ATTR_LEAP_BUTTON_NUMBER, ATTR_SERIAL, CONF_SUBTYPE, DOMAIN, @@ -35,6 +35,12 @@ from .const import ( ) from .models import LutronCasetaData + +def _reverse_dict(forward_dict: dict) -> dict: + """Reverse a dictionary.""" + return {v: k for k, v in forward_dict.items()} + + SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE] LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( @@ -52,9 +58,6 @@ PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP = { "on": 0, "off": 2, } -LEAP_TO_PICO_2_BUTTON_BUTTON_TYPES = { - v: k for k, v in PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP.items() -} PICO_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_2_BUTTON_BUTTON_TYPES_TO_LIP), @@ -74,9 +77,6 @@ PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { "raise": 3, "lower": 4, } -LEAP_TO_PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES = { - v: k for k, v in PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP.items() -} PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In( @@ -96,9 +96,6 @@ PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP = { "stop": 1, "off": 2, } -LEAP_TO_PICO_3_BUTTON_BUTTON_TYPES = { - v: k for k, v in PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP.items() -} PICO_3_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_3_BUTTON_BUTTON_TYPES_TO_LIP), @@ -119,9 +116,6 @@ PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { "raise": 3, "lower": 4, } -LEAP_TO_PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES = { - v: k for k, v in PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP.items() -} PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In( @@ -186,9 +180,6 @@ PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP = { "button_3": 3, "off": 4, } -LEAP_TO_PICO_4_BUTTON_SCENE_BUTTON_TYPES = { - v: k for k, v in PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP.items() -} PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LIP), @@ -208,9 +199,6 @@ PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP = { "group_2_button_1": 3, "group_2_button_2": 4, } -LEAP_TO_PICO_4_BUTTON_2_GROUP_BUTTON_TYPES = { - v: k for k, v in PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP.items() -} PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP), @@ -271,15 +259,58 @@ FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP = { "raise_4": 23, "lower_4": 24, } -LEAP_TO_FOUR_GROUP_REMOTE_BUTTON_TYPES = { - v: k for k, v in FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP.items() -} FOUR_GROUP_REMOTE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP), } ) + +SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, +} +SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In( + SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP + ), + } +) + + +SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, + "button_3": 3, + "raise": 19, + "lower": 18, +} +SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = ( + LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In( + SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP + ), + } + ) +) + +SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, + "button_3": 3, + "button_4": 4, +} +SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In( + SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP + ), + } +) + + DEVICE_TYPE_SCHEMA_MAP = { "Pico2Button": PICO_2_BUTTON_TRIGGER_SCHEMA, "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, @@ -290,6 +321,9 @@ DEVICE_TYPE_SCHEMA_MAP = { "Pico4ButtonZone": PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, "FourGroupRemote": FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, + "SunnataKeypad_2Button": SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA, + "SunnataKeypad_3ButtonRaiseLower": SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, + "SunnataKeypad_4Button": SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA, } DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { @@ -304,16 +338,23 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP, } +DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = { + "Pico2Button": PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP, + "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, + "Pico3Button": PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP, + "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, + "Pico4Button": PICO_4_BUTTON_BUTTON_TYPES_TO_LEAP, + "Pico4ButtonScene": PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP, + "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP, + "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP, + "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP, + "SunnataKeypad_2Button": SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP, + "SunnataKeypad_3ButtonRaiseLower": SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, + "SunnataKeypad_4Button": SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP, +} + LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP = { - "Pico2Button": LEAP_TO_PICO_2_BUTTON_BUTTON_TYPES, - "Pico2ButtonRaiseLower": LEAP_TO_PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES, - "Pico3Button": LEAP_TO_PICO_3_BUTTON_BUTTON_TYPES, - "Pico3ButtonRaiseLower": LEAP_TO_PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES, - "Pico4Button": LEAP_TO_PICO_4_BUTTON_BUTTON_TYPES, - "Pico4ButtonScene": LEAP_TO_PICO_4_BUTTON_SCENE_BUTTON_TYPES, - "Pico4ButtonZone": LEAP_TO_PICO_4_BUTTON_ZONE_BUTTON_TYPES, - "Pico4Button2Group": LEAP_TO_PICO_4_BUTTON_2_GROUP_BUTTON_TYPES, - "FourGroupRemote": LEAP_TO_FOUR_GROUP_REMOTE_BUTTON_TYPES, + k: _reverse_dict(v) for k, v in DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.items() } TRIGGER_SCHEMA = vol.Any( @@ -324,6 +365,9 @@ TRIGGER_SCHEMA = vol.Any( PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, + SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA, + SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, + SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA, ) @@ -354,7 +398,7 @@ async def async_get_triggers( if not (device := get_button_device_by_dr_id(hass, device_id)): raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device["type"], {}) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(device["type"], {}) for trigger in SUPPORTED_INPUTS_EVENTS_TYPES: for subtype in valid_buttons: @@ -389,14 +433,14 @@ async def async_attach_trigger( device_type = _device_model_to_type(device.model) _, serial = list(device.identifiers)[0] schema = DEVICE_TYPE_SCHEMA_MAP.get(device_type) - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device_type) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP[device_type] config = schema(config) event_config = { event_trigger.CONF_PLATFORM: CONF_EVENT, event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, event_trigger.CONF_EVENT_DATA: { ATTR_SERIAL: serial, - ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], + ATTR_LEAP_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], ATTR_ACTION: config[CONF_TYPE], }, } diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index bdf1e359673..54cd842f0ee 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -11,12 +11,12 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.lutron_caseta import ( ATTR_ACTION, ATTR_AREA_NAME, - ATTR_BUTTON_NUMBER, ATTR_DEVICE_NAME, ATTR_SERIAL, ATTR_TYPE, ) from homeassistant.components.lutron_caseta.const import ( + ATTR_LEAP_BUTTON_NUMBER, DOMAIN, LUTRON_CASETA_BUTTON_EVENT, MANUFACTURER, @@ -51,7 +51,23 @@ MOCK_BUTTON_DEVICES = [ "type": "Pico3ButtonRaiseLower", "model": "PJ2-3BRL-GXX-X01", "serial": 43845548, - } + }, + { + "Name": "Front Steps Sunnata Keypad", + "ID": 3, + "Area": {"Name": "Front Steps"}, + "Buttons": [ + {"Number": 7}, + {"Number": 8}, + {"Number": 9}, + {"Number": 10}, + {"Number": 11}, + ], + "leap_name": "Front Steps_Front Steps Sunnata Keypad", + "type": "SunnataKeypad_3ButtonRaiseLower", + "model": "PJ2-3BRL-GXX-X01", + "serial": 43845547, + }, ] @@ -144,12 +160,11 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg): async def test_if_fires_on_button_event(hass, calls, device_reg): """Test for press trigger firing.""" - - config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) - data: LutronCasetaData = hass.data[DOMAIN][config_entry_id] - dr_button_devices = data.button_devices - device_id = list(dr_button_devices)[0] - device = dr_button_devices[device_id] + await _async_setup_lutron_with_picos(hass, device_reg) + device = MOCK_BUTTON_DEVICES[0] + dr = device_registry.async_get(hass) + dr_device = dr.async_get_device(identifiers={(DOMAIN, device["serial"])}) + device_id = dr_device.id assert await async_setup_component( hass, automation.DOMAIN, @@ -175,7 +190,51 @@ async def test_if_fires_on_button_event(hass, calls, device_reg): message = { ATTR_SERIAL: device.get("serial"), ATTR_TYPE: device.get("type"), - ATTR_BUTTON_NUMBER: 2, + ATTR_LEAP_BUTTON_NUMBER: 0, + ATTR_DEVICE_NAME: device["Name"], + ATTR_AREA_NAME: device.get("Area", {}).get("Name"), + ATTR_ACTION: "press", + } + hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_button_press" + + +async def test_if_fires_on_button_event_without_lip(hass, calls, device_reg): + """Test for press trigger firing on a device that does not support lip.""" + await _async_setup_lutron_with_picos(hass, device_reg) + device = MOCK_BUTTON_DEVICES[1] + dr = device_registry.async_get(hass) + dr_device = dr.async_get_device(identifiers={(DOMAIN, device["serial"])}) + device_id = dr_device.id + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "press", + CONF_SUBTYPE: "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + + message = { + ATTR_SERIAL: device.get("serial"), + ATTR_TYPE: device.get("type"), + ATTR_LEAP_BUTTON_NUMBER: 1, ATTR_DEVICE_NAME: device["Name"], ATTR_AREA_NAME: device.get("Area", {}).get("Name"), ATTR_ACTION: "press", @@ -214,7 +273,7 @@ async def test_validate_trigger_config_no_device(hass, calls, device_reg): message = { ATTR_SERIAL: "123", ATTR_TYPE: "any", - ATTR_BUTTON_NUMBER: 3, + ATTR_LEAP_BUTTON_NUMBER: 0, ATTR_DEVICE_NAME: "any", ATTR_AREA_NAME: "area", ATTR_ACTION: "press", @@ -259,7 +318,7 @@ async def test_validate_trigger_config_unknown_device(hass, calls, device_reg): message = { ATTR_SERIAL: "123", ATTR_TYPE: "any", - ATTR_BUTTON_NUMBER: 3, + ATTR_LEAP_BUTTON_NUMBER: 0, ATTR_DEVICE_NAME: "any", ATTR_AREA_NAME: "area", ATTR_ACTION: "press", From 9211ba8371cdc935a6afbcfcf675a0fae6b5bdc5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 1 Jul 2022 19:05:37 +0200 Subject: [PATCH 063/820] Improve type hints in template (#74294) --- .../components/template/binary_sensor.py | 13 +++++---- homeassistant/components/template/light.py | 27 ++++++++++--------- homeassistant/components/template/sensor.py | 2 +- homeassistant/components/template/switch.py | 14 +++++----- homeassistant/components/template/vacuum.py | 4 +-- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index ab7c88e8b8c..bad4a7d7059 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ( @@ -208,16 +209,18 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): ENTITY_ID_FORMAT, object_id, hass=hass ) - self._device_class = config.get(CONF_DEVICE_CLASS) + self._device_class: BinarySensorDeviceClass | None = config.get( + CONF_DEVICE_CLASS + ) self._template = config[CONF_STATE] - self._state = None + self._state: bool | None = None self._delay_cancel = None self._delay_on = None self._delay_on_raw = config.get(CONF_DELAY_ON) self._delay_off = None self._delay_off_raw = config.get(CONF_DELAY_OFF) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore state and register callbacks.""" if ( (self._delay_on_raw is not None or self._delay_off_raw is not None) @@ -283,12 +286,12 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): self._delay_cancel = async_call_later(self.hass, delay, _set_state) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._state @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass | None: """Return the sensor class of the binary sensor.""" return self._device_class diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 807e3e79ef8..f4ad971c1d6 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -197,17 +198,17 @@ class LightTemplate(TemplateEntity, LightEntity): self._supports_transition = False @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of the light.""" return self._brightness @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the CT color value in mireds.""" return self._temperature @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the max mireds value in mireds.""" if self._max_mireds is not None: return self._max_mireds @@ -215,7 +216,7 @@ class LightTemplate(TemplateEntity, LightEntity): return super().max_mireds @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the min mireds value in mireds.""" if self._min_mireds is not None: return self._min_mireds @@ -223,27 +224,27 @@ class LightTemplate(TemplateEntity, LightEntity): return super().min_mireds @property - def white_value(self): + def white_value(self) -> int | None: """Return the white value.""" return self._white_value @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" return self._color @property - def effect(self): + def effect(self) -> str | None: """Return the effect.""" return self._effect @property - def effect_list(self): + def effect_list(self) -> list[str] | None: """Return the effect list.""" return self._effect_list @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = 0 if self._level_script is not None: @@ -261,11 +262,11 @@ class LightTemplate(TemplateEntity, LightEntity): return supported_features @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template: self.add_template_attribute( @@ -345,7 +346,7 @@ class LightTemplate(TemplateEntity, LightEntity): ) await super().async_added_to_hass() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" optimistic_set = False # set optimistic states @@ -448,7 +449,7 @@ class LightTemplate(TemplateEntity, LightEntity): if optimistic_set: self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if ATTR_TRANSITION in kwargs and self._supports_transition is True: await self.async_run_script( diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index ee1ddfa8c2d..8dcda988e9f 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -211,7 +211,7 @@ class SensorTemplate(TemplateSensor): ENTITY_ID_FORMAT, object_id, hass=hass ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.add_template_attribute( "_attr_native_value", self._template, None, self._update_state diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index f04f2b5ba7a..9f282cb9b11 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -1,6 +1,8 @@ """Support for switches which integrates with other components.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.switch import ( @@ -110,7 +112,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self._template = config.get(CONF_VALUE_TEMPLATE) self._on_script = Script(hass, config[ON_ACTION], friendly_name, DOMAIN) self._off_script = Script(hass, config[OFF_ACTION], friendly_name, DOMAIN) - self._state = False + self._state: bool | None = False @callback def _update_state(self, result): @@ -129,7 +131,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self._state = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template is None: @@ -147,18 +149,18 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): await super().async_added_to_hass() @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Fire the on action.""" await self.async_run_script(self._on_script, context=self._context) if self._template is None: self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Fire the off action.""" await self.async_run_script(self._off_script, context=self._context) if self._template is None: @@ -166,6 +168,6 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self.async_write_ha_state() @property - def assumed_state(self): + def assumed_state(self) -> bool: """State is assumed, if no template given.""" return self._template is None diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 5f306bfa5e1..0a74ee5c5fc 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -200,7 +200,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] @property - def state(self): + def state(self) -> str | None: """Return the status of the vacuum cleaner.""" return self._state @@ -263,7 +263,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template is not None: self.add_template_attribute( From 1288085b31eb5bf15fb0e8f777b4101b168ec616 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 12:10:40 -0500 Subject: [PATCH 064/820] Revert scrape changes to 2022.6.6 (#74305) --- CODEOWNERS | 4 +- homeassistant/components/scrape/__init__.py | 63 ------ homeassistant/components/scrape/manifest.json | 3 +- homeassistant/components/scrape/sensor.py | 104 ++++----- homeassistant/generated/config_flows.py | 1 - tests/components/scrape/__init__.py | 40 +--- tests/components/scrape/test_config_flow.py | 194 ---------------- tests/components/scrape/test_init.py | 89 -------- tests/components/scrape/test_sensor.py | 209 ++++++++---------- 9 files changed, 137 insertions(+), 570 deletions(-) delete mode 100644 tests/components/scrape/test_config_flow.py delete mode 100644 tests/components/scrape/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 9845f5f7e5d..0c3f73db76d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -893,8 +893,8 @@ build.json @home-assistant/supervisor /homeassistant/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core /homeassistant/components/schluter/ @prairieapps -/homeassistant/components/scrape/ @fabaff @gjohansson-ST -/tests/components/scrape/ @fabaff @gjohansson-ST +/homeassistant/components/scrape/ @fabaff +/tests/components/scrape/ @fabaff /homeassistant/components/screenlogic/ @dieselrabbit @bdraco /tests/components/screenlogic/ @dieselrabbit @bdraco /homeassistant/components/script/ @home-assistant/core diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index 684be76b80d..f9222c126b5 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -1,64 +1 @@ """The scrape component.""" -from __future__ import annotations - -import httpx - -from homeassistant.components.rest.data import RestData -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_AUTHENTICATION, - CONF_HEADERS, - CONF_PASSWORD, - CONF_RESOURCE, - CONF_USERNAME, - CONF_VERIFY_SSL, - HTTP_DIGEST_AUTHENTICATION, -) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady - -from .const import DOMAIN, PLATFORMS - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Scrape from a config entry.""" - - resource: str = entry.options[CONF_RESOURCE] - method: str = "GET" - payload: str | None = None - headers: str | None = entry.options.get(CONF_HEADERS) - verify_ssl: bool = entry.options[CONF_VERIFY_SSL] - username: str | None = entry.options.get(CONF_USERNAME) - password: str | None = entry.options.get(CONF_PASSWORD) - - auth: httpx.DigestAuth | tuple[str, str] | None = None - if username and password: - if entry.options.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = httpx.DigestAuth(username, password) - else: - auth = (username, password) - - rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) - await rest.async_update() - - if rest.data is None: - raise ConfigEntryNotReady - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = rest - - entry.async_on_unload(entry.add_update_listener(async_update_listener)) - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener for options.""" - await hass.config_entries.async_reload(entry.entry_id) - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Scrape config entry.""" - - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 631af2e6051..b1ccbb354a9 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], "after_dependencies": ["rest"], - "codeowners": ["@fabaff", "@gjohansson-ST"], - "config_flow": true, + "codeowners": ["@fabaff"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b6b8828ca73..e15f7c5ba97 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,11 +1,11 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations -from datetime import timedelta import logging from typing import Any from bs4 import BeautifulSoup +import httpx import voluptuous as vol from homeassistant.components.rest.data import RestData @@ -16,16 +16,13 @@ from homeassistant.components.sensor import ( STATE_CLASSES_SCHEMA, SensorEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( - CONF_ATTRIBUTE, CONF_AUTHENTICATION, CONF_DEVICE_CLASS, CONF_HEADERS, CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, - CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, @@ -34,25 +31,26 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN - _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) -ICON = "mdi:web" +CONF_ATTR = "attribute" +CONF_SELECT = "select" +CONF_INDEX = "index" + +DEFAULT_NAME = "Web scrape" +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.string, vol.Required(CONF_SELECT): cv.string, - vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_ATTR): cv.string, vol.Optional(CONF_INDEX, default=0): cv.positive_int, vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] @@ -64,7 +62,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } ) @@ -77,47 +75,37 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Web scrape sensor.""" - _LOGGER.warning( - # Config flow added in Home Assistant Core 2022.7, remove import flow in 2022.9 - "Loading Scrape via platform setup has been deprecated in Home Assistant 2022.7 " - "Your configuration has been automatically imported and you can " - "remove it from your configuration.yaml" - ) + name: str = config[CONF_NAME] + resource: str = config[CONF_RESOURCE] + method: str = "GET" + payload: str | None = None + headers: str | None = config.get(CONF_HEADERS) + verify_ssl: bool = config[CONF_VERIFY_SSL] + select: str | None = config.get(CONF_SELECT) + attr: str | None = config.get(CONF_ATTR) + index: int = config[CONF_INDEX] + unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) + device_class: str | None = config.get(CONF_DEVICE_CLASS) + state_class: str | None = config.get(CONF_STATE_CLASS) + username: str | None = config.get(CONF_USERNAME) + password: str | None = config.get(CONF_PASSWORD) + value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if config.get(CONF_VALUE_TEMPLATE): - template: Template = Template(config[CONF_VALUE_TEMPLATE]) - template.ensure_valid() - config[CONF_VALUE_TEMPLATE] = template.template - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={k: v for k, v in config.items() if k != CONF_SCAN_INTERVAL}, - ) - ) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the Scrape sensor entry.""" - name: str = entry.options[CONF_NAME] - resource: str = entry.options[CONF_RESOURCE] - select: str | None = entry.options.get(CONF_SELECT) - attr: str | None = entry.options.get(CONF_ATTRIBUTE) - index: int = int(entry.options[CONF_INDEX]) - unit: str | None = entry.options.get(CONF_UNIT_OF_MEASUREMENT) - device_class: str | None = entry.options.get(CONF_DEVICE_CLASS) - state_class: str | None = entry.options.get(CONF_STATE_CLASS) - value_template: str | None = entry.options.get(CONF_VALUE_TEMPLATE) - entry_id: str = entry.entry_id - - val_template: Template | None = None if value_template is not None: - val_template = Template(value_template, hass) + value_template.hass = hass - rest = hass.data[DOMAIN][entry.entry_id] + auth: httpx.DigestAuth | tuple[str, str] | None = None + if username and password: + if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: + auth = httpx.DigestAuth(username, password) + else: + auth = (username, password) + + rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) + await rest.async_update() + + if rest.data is None: + raise PlatformNotReady async_add_entities( [ @@ -127,12 +115,10 @@ async def async_setup_entry( select, attr, index, - val_template, + value_template, unit, device_class, state_class, - entry_id, - resource, ) ], True, @@ -142,8 +128,6 @@ async def async_setup_entry( class ScrapeSensor(SensorEntity): """Representation of a web scrape sensor.""" - _attr_icon = ICON - def __init__( self, rest: RestData, @@ -155,8 +139,6 @@ class ScrapeSensor(SensorEntity): unit: str | None, device_class: str | None, state_class: str | None, - entry_id: str, - resource: str, ) -> None: """Initialize a web scrape sensor.""" self.rest = rest @@ -169,14 +151,6 @@ class ScrapeSensor(SensorEntity): self._attr_native_unit_of_measurement = unit self._attr_device_class = device_class self._attr_state_class = state_class - self._attr_unique_id = entry_id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, entry_id)}, - manufacturer="Scrape", - name=name, - configuration_url=resource, - ) def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0b985f6e161..eba8bfe10c4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -301,7 +301,6 @@ FLOWS = { "ruckus_unleashed", "sabnzbd", "samsungtv", - "scrape", "screenlogic", "season", "sense", diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py index 37abb061e75..0ba9266a79d 100644 --- a/tests/components/scrape/__init__.py +++ b/tests/components/scrape/__init__.py @@ -2,42 +2,6 @@ from __future__ import annotations from typing import Any -from unittest.mock import patch - -from homeassistant.components.scrape.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def init_integration( - hass: HomeAssistant, - config: dict[str, Any], - data: str, - entry_id: str = "1", - source: str = SOURCE_USER, -) -> MockConfigEntry: - """Set up the Scrape integration in Home Assistant.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - source=source, - data={}, - options=config, - entry_id=entry_id, - ) - - config_entry.add_to_hass(hass) - mocker = MockRestData(data) - with patch( - "homeassistant.components.scrape.RestData", - return_value=mocker, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry def return_config( @@ -61,8 +25,6 @@ def return_config( "resource": "https://www.home-assistant.io", "select": select, "name": name, - "index": 0, - "verify_ssl": True, } if attribute: config["attribute"] = attribute @@ -76,7 +38,7 @@ def return_config( config["device_class"] = device_class if state_class: config["state_class"] = state_class - if username: + if authentication: config["authentication"] = authentication config["username"] = username config["password"] = password diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py deleted file mode 100644 index 287004b1dd3..00000000000 --- a/tests/components/scrape/test_config_flow.py +++ /dev/null @@ -1,194 +0,0 @@ -"""Test the Scrape config flow.""" -from __future__ import annotations - -from unittest.mock import patch - -from homeassistant import config_entries -from homeassistant.components.scrape.const import CONF_INDEX, CONF_SELECT, DOMAIN -from homeassistant.const import ( - CONF_NAME, - CONF_RESOURCE, - CONF_VALUE_TEMPLATE, - CONF_VERIFY_SSL, -) -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) - -from . import MockRestData - -from tests.common import MockConfigEntry - - -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_FORM - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Release" - assert result2["options"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0.0, - "verify_ssl": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_success(hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - CONF_INDEX: 0, - CONF_VERIFY_SSL: True, - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Release" - assert result2["options"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - }, - ).add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - CONF_INDEX: 0, - CONF_VERIFY_SSL: True, - }, - ) - await hass.async_block_till_done() - - assert result3["type"] == RESULT_TYPE_ABORT - assert result3["reason"] == "already_configured" - - -async def test_options_form(hass: HomeAssistant) -> None: - """Test we get the form in options.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - }, - entry_id="1", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "value_template": "{{ value.split(':')[1] }}", - "index": 1.0, - "verify_ssl": True, - }, - ) - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 1.0, - "verify_ssl": True, - } - entry_check = hass.config_entries.async_get_entry("1") - assert entry_check.state == config_entries.ConfigEntryState.LOADED - assert entry_check.update_listeners is not None diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py deleted file mode 100644 index 021790e65c3..00000000000 --- a/tests/components/scrape/test_init.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Test Scrape component setup process.""" -from __future__ import annotations - -from unittest.mock import patch - -from homeassistant.components.scrape.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant - -from . import MockRestData - -from tests.common import MockConfigEntry - -TEST_CONFIG = { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, -} - - -async def test_setup_entry(hass: HomeAssistant) -> None: - """Test setup entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options=TEST_CONFIG, - title="Release", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert state - - -async def test_setup_entry_no_data_fails(hass: HomeAssistant) -> None: - """Test setup entry no data fails.""" - entry = MockConfigEntry( - domain=DOMAIN, data={}, options=TEST_CONFIG, title="Release", entry_id="1" - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor_no_data"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.ha_version") - assert state is None - entry = hass.config_entries.async_get_entry("1") - assert entry.state == ConfigEntryState.SETUP_RETRY - - -async def test_remove_entry(hass: HomeAssistant) -> None: - """Test remove entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options=TEST_CONFIG, - title="Release", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert state - - await hass.config_entries.async_remove(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert not state diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index cd4e27e88a2..aaf156208ef 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -3,15 +3,10 @@ from __future__ import annotations from unittest.mock import patch -import pytest - from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor.const import CONF_STATE_CLASS -from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_DEVICE_CLASS, - CONF_NAME, - CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, @@ -20,20 +15,22 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component -from . import MockRestData, init_integration, return_config - -from tests.common import MockConfigEntry +from . import MockRestData, return_config DOMAIN = "scrape" async def test_scrape_sensor(hass: HomeAssistant) -> None: """Test Scrape sensor minimal.""" - await init_integration( - hass, - return_config(select=".current-version h1", name="HA version"), - "test_scrape_sensor", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state.state == "Current Version: 2021.12.10" @@ -41,15 +38,21 @@ async def test_scrape_sensor(hass: HomeAssistant) -> None: async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: """Test Scrape sensor with value template.""" - await init_integration( - hass, - return_config( + config = { + "sensor": return_config( select=".current-version h1", name="HA version", template="{{ value.split(':')[1] }}", - ), - "test_scrape_sensor", - ) + ) + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state.state == "2021.12.10" @@ -57,18 +60,24 @@ async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: """Test Scrape sensor for unit of measurement, device class and state class.""" - await init_integration( - hass, - return_config( + config = { + "sensor": return_config( select=".current-temp h3", name="Current Temp", template="{{ value.split(':')[1] }}", uom="°C", device_class="temperature", state_class="measurement", - ), - "test_scrape_uom_and_classes", - ) + ) + } + + mocker = MockRestData("test_scrape_uom_and_classes") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.current_temp") assert state.state == "22.1" @@ -79,28 +88,31 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: """Test Scrape sensor with authentication.""" - await init_integration( - hass, - return_config( - select=".return", - name="Auth page", - username="user@secret.com", - password="12345678", - authentication="digest", - ), - "test_scrape_sensor_authentication", - ) - await init_integration( - hass, - return_config( - select=".return", - name="Auth page2", - username="user@secret.com", - password="12345678", - ), - "test_scrape_sensor_authentication", - entry_id="2", - ) + config = { + "sensor": [ + return_config( + select=".return", + name="Auth page", + username="user@secret.com", + password="12345678", + authentication="digest", + ), + return_config( + select=".return", + name="Auth page2", + username="user@secret.com", + password="12345678", + ), + ] + } + + mocker = MockRestData("test_scrape_sensor_authentication") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.auth_page") assert state.state == "secret text" @@ -110,11 +122,15 @@ async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: """Test Scrape sensor fails on no data.""" - await init_integration( - hass, - return_config(select=".current-version h1", name="HA version"), - "test_scrape_sensor_no_data", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} + + mocker = MockRestData("test_scrape_sensor_no_data") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state is None @@ -122,21 +138,14 @@ async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: """Test Scrape sensor no data on refresh.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data={}, - options=return_config(select=".current-version h1", name="HA version"), - entry_id="1", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} - config_entry.add_to_hass(hass) mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.RestData", + "homeassistant.components.scrape.sensor.RestData", return_value=mocker, ): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") @@ -153,17 +162,20 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: """Test Scrape sensor with attribute and tag.""" - await init_integration( - hass, - return_config(select="div", name="HA class", index=1, attribute="class"), - "test_scrape_sensor", - ) - await init_integration( - hass, - return_config(select="template", name="HA template"), - "test_scrape_sensor", - entry_id="2", - ) + config = { + "sensor": [ + return_config(select="div", name="HA class", index=1, attribute="class"), + return_config(select="template", name="HA template"), + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_class") assert state.state == "['links']" @@ -173,55 +185,22 @@ async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: async def test_scrape_sensor_errors(hass: HomeAssistant) -> None: """Test Scrape sensor handle errors.""" - await init_integration( - hass, - return_config(select="div", name="HA class", index=5, attribute="class"), - "test_scrape_sensor", - ) - await init_integration( - hass, - return_config(select="div", name="HA class2", attribute="classes"), - "test_scrape_sensor", - entry_id="2", - ) - - state = hass.states.get("sensor.ha_class") - assert state.state == STATE_UNKNOWN - state2 = hass.states.get("sensor.ha_class2") - assert state2.state == STATE_UNKNOWN - - -async def test_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: - """Test the Scrape sensor import.""" config = { - "sensor": { - "platform": "scrape", - "resource": "https://www.home-assistant.io", - "select": ".current-version h1", - "name": "HA Version", - "index": 0, - "verify_ssl": True, - "value_template": "{{ value.split(':')[1] }}", - } + "sensor": [ + return_config(select="div", name="HA class", index=5, attribute="class"), + return_config(select="div", name="HA class2", attribute="classes"), + ] } mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.RestData", + "homeassistant.components.scrape.sensor.RestData", return_value=mocker, ): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - assert ( - "Loading Scrape via platform setup has been deprecated in Home Assistant" - in caplog.text - ) - - assert hass.config_entries.async_entries(DOMAIN) - options = hass.config_entries.async_entries(DOMAIN)[0].options - assert options[CONF_NAME] == "HA Version" - assert options[CONF_RESOURCE] == "https://www.home-assistant.io" - - state = hass.states.get("sensor.ha_version") - assert state.state == "2021.12.10" + state = hass.states.get("sensor.ha_class") + assert state.state == STATE_UNKNOWN + state2 = hass.states.get("sensor.ha_class2") + assert state2.state == STATE_UNKNOWN From 810e29f1ef668ae7d2a2c74188eb243949a62040 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 11:01:07 -0700 Subject: [PATCH 065/820] Guard creating areas in onboarding (#74306) --- homeassistant/components/onboarding/views.py | 8 +++++--- tests/components/onboarding/test_views.py | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 7f40ad87e84..c29fb7edf3a 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -156,9 +156,11 @@ class UserOnboardingView(_BaseOnboardingView): area_registry = ar.async_get(hass) for area in DEFAULT_AREAS: - area_registry.async_create( - translations[f"component.onboarding.area.{area}"] - ) + name = translations[f"component.onboarding.area.{area}"] + # Guard because area might have been created by an automatically + # set up integration. + if not area_registry.async_get_area_by_name(name): + area_registry.async_create(name) await self._async_mark_done(hass) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 982f5b86e65..204eb6bf772 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -144,6 +144,12 @@ async def test_onboarding_user_already_done(hass, hass_storage, hass_client_no_a async def test_onboarding_user(hass, hass_storage, hass_client_no_auth): """Test creating a new user.""" + area_registry = ar.async_get(hass) + + # Create an existing area to mimic an integration creating an area + # before onboarding is done. + area_registry.async_create("Living Room") + assert await async_setup_component(hass, "person", {}) assert await async_setup_component(hass, "onboarding", {}) await hass.async_block_till_done() @@ -194,7 +200,6 @@ async def test_onboarding_user(hass, hass_storage, hass_client_no_auth): ) # Validate created areas - area_registry = ar.async_get(hass) assert len(area_registry.areas) == 3 assert sorted(area.name for area in area_registry.async_list_areas()) == [ "Bedroom", From 647a023776bedb5e1605390c91b8a89e5a68dfff Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 2 Jul 2022 00:25:27 +0000 Subject: [PATCH 066/820] [ci skip] Translation update --- .../components/adguard/translations/hu.json | 2 +- .../components/anthemav/translations/ca.json | 19 ++++++++++++++ .../components/anthemav/translations/hu.json | 19 ++++++++++++++ .../components/anthemav/translations/ja.json | 19 ++++++++++++++ .../components/anthemav/translations/no.json | 19 ++++++++++++++ .../anthemav/translations/pt-BR.json | 19 ++++++++++++++ .../components/anthemav/translations/tr.json | 19 ++++++++++++++ .../components/awair/translations/tr.json | 7 ++++++ .../components/climacell/translations/hu.json | 2 +- .../components/esphome/translations/ca.json | 4 +-- .../components/esphome/translations/et.json | 4 +-- .../components/esphome/translations/no.json | 4 +-- .../esphome/translations/pt-BR.json | 4 +-- .../components/esphome/translations/tr.json | 6 ++--- .../esphome/translations/zh-Hant.json | 4 +-- .../components/hive/translations/tr.json | 9 ++++++- .../components/lcn/translations/hu.json | 1 + .../components/lcn/translations/tr.json | 1 + .../lg_soundbar/translations/ca.json | 18 +++++++++++++ .../lg_soundbar/translations/et.json | 18 +++++++++++++ .../lg_soundbar/translations/hu.json | 18 +++++++++++++ .../lg_soundbar/translations/ja.json | 18 +++++++++++++ .../lg_soundbar/translations/no.json | 18 +++++++++++++ .../lg_soundbar/translations/pt-BR.json | 18 +++++++++++++ .../lg_soundbar/translations/tr.json | 18 +++++++++++++ .../lg_soundbar/translations/zh-Hant.json | 18 +++++++++++++ .../components/life360/translations/et.json | 2 +- .../components/life360/translations/hu.json | 23 +++++++++++++++++ .../components/life360/translations/tr.json | 25 ++++++++++++++++++- .../components/nest/translations/ja.json | 1 + .../components/nest/translations/tr.json | 2 +- .../components/nina/translations/hu.json | 22 ++++++++++++++++ .../components/nina/translations/tr.json | 22 ++++++++++++++++ .../overkiz/translations/sensor.tr.json | 5 ++++ .../components/plugwise/translations/tr.json | 4 +-- .../components/qnap_qsw/translations/ca.json | 6 +++++ .../components/qnap_qsw/translations/de.json | 6 +++++ .../components/qnap_qsw/translations/fr.json | 6 +++++ .../components/qnap_qsw/translations/hu.json | 6 +++++ .../components/qnap_qsw/translations/ja.json | 6 +++++ .../components/qnap_qsw/translations/no.json | 6 +++++ .../qnap_qsw/translations/pt-BR.json | 6 +++++ .../components/qnap_qsw/translations/tr.json | 6 +++++ .../qnap_qsw/translations/zh-Hant.json | 6 +++++ .../simplepush/translations/tr.json | 21 ++++++++++++++++ .../soundtouch/translations/ca.json | 21 ++++++++++++++++ .../soundtouch/translations/de.json | 21 ++++++++++++++++ .../soundtouch/translations/et.json | 21 ++++++++++++++++ .../soundtouch/translations/fr.json | 21 ++++++++++++++++ .../soundtouch/translations/hu.json | 21 ++++++++++++++++ .../soundtouch/translations/ja.json | 21 ++++++++++++++++ .../soundtouch/translations/no.json | 21 ++++++++++++++++ .../soundtouch/translations/pt-BR.json | 21 ++++++++++++++++ .../soundtouch/translations/tr.json | 21 ++++++++++++++++ .../soundtouch/translations/zh-Hant.json | 21 ++++++++++++++++ 55 files changed, 676 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/ca.json create mode 100644 homeassistant/components/anthemav/translations/hu.json create mode 100644 homeassistant/components/anthemav/translations/ja.json create mode 100644 homeassistant/components/anthemav/translations/no.json create mode 100644 homeassistant/components/anthemav/translations/pt-BR.json create mode 100644 homeassistant/components/anthemav/translations/tr.json create mode 100644 homeassistant/components/lg_soundbar/translations/ca.json create mode 100644 homeassistant/components/lg_soundbar/translations/et.json create mode 100644 homeassistant/components/lg_soundbar/translations/hu.json create mode 100644 homeassistant/components/lg_soundbar/translations/ja.json create mode 100644 homeassistant/components/lg_soundbar/translations/no.json create mode 100644 homeassistant/components/lg_soundbar/translations/pt-BR.json create mode 100644 homeassistant/components/lg_soundbar/translations/tr.json create mode 100644 homeassistant/components/lg_soundbar/translations/zh-Hant.json create mode 100644 homeassistant/components/simplepush/translations/tr.json create mode 100644 homeassistant/components/soundtouch/translations/ca.json create mode 100644 homeassistant/components/soundtouch/translations/de.json create mode 100644 homeassistant/components/soundtouch/translations/et.json create mode 100644 homeassistant/components/soundtouch/translations/fr.json create mode 100644 homeassistant/components/soundtouch/translations/hu.json create mode 100644 homeassistant/components/soundtouch/translations/ja.json create mode 100644 homeassistant/components/soundtouch/translations/no.json create mode 100644 homeassistant/components/soundtouch/translations/pt-BR.json create mode 100644 homeassistant/components/soundtouch/translations/tr.json create mode 100644 homeassistant/components/soundtouch/translations/zh-Hant.json diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index ffbf454b62b..59f33efee29 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "existing_instance_updated": "Friss\u00edtette a megl\u00e9v\u0151 konfigur\u00e1ci\u00f3t." + "existing_instance_updated": "A megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 friss\u00edtve." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/anthemav/translations/ca.json b/homeassistant/components/anthemav/translations/ca.json new file mode 100644 index 00000000000..723883d5c1a --- /dev/null +++ b/homeassistant/components/anthemav/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "cannot_receive_deviceinfo": "No s'ha pogut obtenir l'adre\u00e7a MAC. Assegura't que el dispositiu estigui enc\u00e8s" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/hu.json b/homeassistant/components/anthemav/translations/hu.json new file mode 100644 index 00000000000..f13544fff61 --- /dev/null +++ b/homeassistant/components/anthemav/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "cannot_receive_deviceinfo": "Nem siker\u00fclt lek\u00e9rni a MAC-c\u00edmet. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a k\u00e9sz\u00fcl\u00e9k be van kapcsolva" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ja.json b/homeassistant/components/anthemav/translations/ja.json new file mode 100644 index 00000000000..8c87d02e557 --- /dev/null +++ b/homeassistant/components/anthemav/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "cannot_receive_deviceinfo": "MAC\u30a2\u30c9\u30ec\u30b9\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/no.json b/homeassistant/components/anthemav/translations/no.json new file mode 100644 index 00000000000..e7b3f66ae8d --- /dev/null +++ b/homeassistant/components/anthemav/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "cannot_receive_deviceinfo": "Kunne ikke hente MAC-adressen. S\u00f8rg for at enheten er sl\u00e5tt p\u00e5" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json new file mode 100644 index 00000000000..309aca9b8ef --- /dev/null +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "cannot_receive_deviceinfo": "Falha ao recuperar o endere\u00e7o MAC. Verifique se o dispositivo est\u00e1 ligado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/tr.json b/homeassistant/components/anthemav/translations/tr.json new file mode 100644 index 00000000000..cbe85a5319c --- /dev/null +++ b/homeassistant/components/anthemav/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "cannot_receive_deviceinfo": "MAC Adresi al\u0131namad\u0131. Cihaz\u0131n a\u00e7\u0131k oldu\u011fundan emin olun" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index e8fa8ca1027..ef34620b5f7 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -17,6 +17,13 @@ }, "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." }, + "reauth_confirm": { + "data": { + "access_token": "Eri\u015fim Anahtar\u0131", + "email": "E-posta" + }, + "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." + }, "user": { "data": { "access_token": "Eri\u015fim Anahtar\u0131", diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json index f65fa638ced..4cad1eaaa0f 100644 --- a/homeassistant/components/climacell/translations/hu.json +++ b/homeassistant/components/climacell/translations/hu.json @@ -6,7 +6,7 @@ "timestep": "Min. A NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt" }, "description": "Ha a `nowcast` el\u0151rejelz\u00e9si entit\u00e1s enged\u00e9lyez\u00e9s\u00e9t v\u00e1lasztja, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tti percek sz\u00e1m\u00e1t. A megadott el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt kiv\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.", - "title": "[%key:component::climacell::title%] be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" + "title": "ClimaCell be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" } } } diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index 4c990994e47..ef9814d3ac8 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -9,7 +9,7 @@ "connection_error": "No s'ha pogut connectar amb ESP. Verifica que l'arxiu YAML cont\u00e9 la l\u00ednia 'api:'.", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_psk": "La clau de xifratge de transport \u00e9s inv\u00e0lida. Assegura't que coincideix amb la de la configuraci\u00f3", - "resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Amfitri\u00f3", "port": "Port" }, - "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu node [ESPHome](https://esphomelib.com/)." + "description": "Introdueix la configuraci\u00f3 de connexi\u00f3 del node [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/et.json b/homeassistant/components/esphome/translations/et.json index ea5119b190d..29fa98bafb0 100644 --- a/homeassistant/components/esphome/translations/et.json +++ b/homeassistant/components/esphome/translations/et.json @@ -9,7 +9,7 @@ "connection_error": "ESP-ga ei saa \u00fchendust luua. Veendu, et YAML-fail sisaldab rida 'api:'.", "invalid_auth": "Tuvastamise viga", "invalid_psk": "\u00dclekande kr\u00fcpteerimisv\u00f5ti on kehtetu. Veendu, et see vastab seadetes sisalduvale", - "resolve_error": "ESP aadressi ei \u00f5nnestu lahendada. Kui see viga p\u00fcsib, m\u00e4\u00e4ra staatiline IP-aadress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "ESP aadressi ei saa lahendada. Kui see t\u00f5rge p\u00fcsib, m\u00e4\u00e4ra staatiline IP-aadress" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "", "port": "Port" }, - "description": "Sisesta oma [ESPHome](https://esphomelib.com/) s\u00f5lme \u00fchenduse s\u00e4tted." + "description": "Sisesta oma [ESPHome]({esphome_url}) s\u00f5lme \u00fchenduse s\u00e4tted." } } } diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 0d583893570..1fabb774aa2 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -9,7 +9,7 @@ "connection_error": "Kan ikke koble til ESP. Kontroller at YAML filen din inneholder en \"api:\" linje.", "invalid_auth": "Ugyldig godkjenning", "invalid_psk": "Transportkrypteringsn\u00f8kkelen er ugyldig. S\u00f8rg for at den samsvarer med det du har i konfigurasjonen", - "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, vennligst [sett en statisk IP-adresse](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)" + "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, vennligst angi en statisk IP-adresse" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Vert", "port": "Port" }, - "description": "Vennligst fyll inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node." + "description": "Vennligst skriv inn tilkoblingsinnstillingene for [ESPHome]( {esphome_url} )-noden." } } } diff --git a/homeassistant/components/esphome/translations/pt-BR.json b/homeassistant/components/esphome/translations/pt-BR.json index 737bc5020af..21fd9067be1 100644 --- a/homeassistant/components/esphome/translations/pt-BR.json +++ b/homeassistant/components/esphome/translations/pt-BR.json @@ -9,7 +9,7 @@ "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_psk": "A chave de criptografia de transporte \u00e9 inv\u00e1lida. Certifique-se de que corresponde ao que voc\u00ea tem em sua configura\u00e7\u00e3o", - "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, por favor, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se o erro persistir, defina um endere\u00e7o IP est\u00e1tico" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Nome do host", "port": "Porta" }, - "description": "Por favor insira as configura\u00e7\u00f5es de conex\u00e3o de seu n\u00f3 de [ESPHome] (https://esphomelib.com/)." + "description": "Insira as configura\u00e7\u00f5es de conex\u00e3o do seu n\u00f3 [ESPHome]( {esphome_url} )." } } } diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index b2cede2b572..d4388dde8b8 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -9,7 +9,7 @@ "connection_error": "ESP'ye ba\u011flan\u0131lam\u0131yor. L\u00fctfen YAML dosyan\u0131z\u0131n bir 'api:' sat\u0131r\u0131 i\u00e7erdi\u011finden emin olun.", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_psk": "Aktar\u0131m \u015fifreleme anahtar\u0131 ge\u00e7ersiz. L\u00fctfen yap\u0131land\u0131rman\u0131zda sahip oldu\u011funuzla e\u015fle\u015fti\u011finden emin olun", - "resolve_error": "ESP'nin adresi \u00e7\u00f6z\u00fclemiyor. Bu hata devam ederse, l\u00fctfen statik bir IP adresi ayarlay\u0131n: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "ESP'nin adresi \u00e7\u00f6z\u00fclemiyor. Bu hata devam ederse, l\u00fctfen statik bir IP adresi ayarlay\u0131n" }, "flow_title": "{name}", "step": { @@ -21,7 +21,7 @@ }, "discovery_confirm": { "description": "ESPHome d\u00fc\u011f\u00fcm\u00fcn\u00fc ` {name} ` Home Assistant'a eklemek istiyor musunuz?", - "title": "Ke\u015ffedilen ESPHome d\u00fc\u011f\u00fcm\u00fc" + "title": "ESPHome d\u00fc\u011f\u00fcm\u00fc ke\u015ffedildi" }, "encryption_key": { "data": { @@ -40,7 +40,7 @@ "host": "Sunucu", "port": "Port" }, - "description": "L\u00fctfen [ESPHome](https://esphomelib.com/) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." + "description": "L\u00fctfen [ESPHome]( {esphome_url} ) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." } } } diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 976d0317faa..44f50a433e3 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -9,7 +9,7 @@ "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 ESP\uff0c\u8acb\u78ba\u5b9a\u60a8\u7684 YAML \u6a94\u6848\u5305\u542b\u300capi:\u300d\u8a2d\u5b9a\u5217\u3002", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_psk": "\u50b3\u8f38\u91d1\u9470\u7121\u6548\u3002\u8acb\u78ba\u5b9a\u8207\u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u91d1\u9470\u76f8\u7b26\u5408", - "resolve_error": "\u7121\u6cd5\u89e3\u6790 ESP \u4f4d\u5740\uff0c\u5047\u5982\u6b64\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u53c3\u8003\u8aaa\u660e\u8a2d\u5b9a\u70ba\u975c\u614b\u56fa\u5b9a IP \uff1a https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "\u7121\u6cd5\u89e3\u6790 ESP \u4f4d\u5740\uff0c\u5047\u5982\u6b64\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u53c3\u8003\u8aaa\u660e\u8a2d\u5b9a\u70ba\u975c\u614b\u56fa\u5b9a IP" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "description": "\u8acb\u8f38\u5165 [ESPHome](https://esphomelib.com/) \u7bc0\u9ede\u9023\u7dda\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 [ESPHome]({esphome_url}) \u7bc0\u9ede\u9023\u7dda\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/hive/translations/tr.json b/homeassistant/components/hive/translations/tr.json index afc0ee66f03..158424f996a 100644 --- a/homeassistant/components/hive/translations/tr.json +++ b/homeassistant/components/hive/translations/tr.json @@ -20,6 +20,13 @@ "description": "Hive kimlik do\u011frulama kodunuzu girin. \n\n Ba\u015fka bir kod istemek i\u00e7in l\u00fctfen 0000 kodunu girin.", "title": "Hive \u0130ki Fakt\u00f6rl\u00fc Kimlik Do\u011frulama." }, + "configuration": { + "data": { + "device_name": "Cihaz ad\u0131" + }, + "description": "Hive yap\u0131land\u0131rman\u0131z\u0131 girin", + "title": "Hive Yap\u0131land\u0131rmas\u0131." + }, "reauth": { "data": { "password": "Parola", @@ -34,7 +41,7 @@ "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Hive oturum a\u00e7ma bilgilerinizi ve yap\u0131land\u0131rman\u0131z\u0131 girin.", + "description": "Hive giri\u015f bilgilerinizi girin.", "title": "Hive Giri\u015f yap" } } diff --git a/homeassistant/components/lcn/translations/hu.json b/homeassistant/components/lcn/translations/hu.json index 3e56a4a149b..f2b7566d838 100644 --- a/homeassistant/components/lcn/translations/hu.json +++ b/homeassistant/components/lcn/translations/hu.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "k\u00f3dz\u00e1rol\u00e1si k\u00f3d \u00e9rkezett", "fingerprint": "ujjlenyomatk\u00f3d \u00e9rkezett", "send_keys": "kulcs/gomb \u00e9rkezett", "transmitter": "t\u00e1vvez\u00e9rl\u0151 k\u00f3d \u00e9rkezett", diff --git a/homeassistant/components/lcn/translations/tr.json b/homeassistant/components/lcn/translations/tr.json index 2c2d94611d0..f3a40e165c6 100644 --- a/homeassistant/components/lcn/translations/tr.json +++ b/homeassistant/components/lcn/translations/tr.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "kod kilit kodu al\u0131nd\u0131", "fingerprint": "al\u0131nan parmak izi kodu", "send_keys": "al\u0131nan anahtarlar\u0131 g\u00f6nder", "transmitter": "verici kodu al\u0131nd\u0131", diff --git a/homeassistant/components/lg_soundbar/translations/ca.json b/homeassistant/components/lg_soundbar/translations/ca.json new file mode 100644 index 00000000000..8c445361eb8 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/et.json b/homeassistant/components/lg_soundbar/translations/et.json new file mode 100644 index 00000000000..227250382c0 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "existing_instance_updated": "V\u00e4rskendati olemasolevat konfiguratsiooni." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/hu.json b/homeassistant/components/lg_soundbar/translations/hu.json new file mode 100644 index 00000000000..c39033d273a --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "existing_instance_updated": "A megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 friss\u00edtve." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ja.json b/homeassistant/components/lg_soundbar/translations/ja.json new file mode 100644 index 00000000000..cd40cd88044 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "existing_instance_updated": "\u65e2\u5b58\u306e\u69cb\u6210\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/no.json b/homeassistant/components/lg_soundbar/translations/no.json new file mode 100644 index 00000000000..41eef0e4c16 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "existing_instance_updated": "Oppdatert eksisterende konfigurasjon." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/pt-BR.json b/homeassistant/components/lg_soundbar/translations/pt-BR.json new file mode 100644 index 00000000000..dfbff8cddc8 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/tr.json b/homeassistant/components/lg_soundbar/translations/tr.json new file mode 100644 index 00000000000..5eb581847fb --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "existing_instance_updated": "Mevcut yap\u0131land\u0131rma g\u00fcncellendi." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/zh-Hant.json b/homeassistant/components/lg_soundbar/translations/zh-Hant.json new file mode 100644 index 00000000000..8680a863901 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/et.json b/homeassistant/components/life360/translations/et.json index c6e66af5d08..360aa8275f7 100644 --- a/homeassistant/components/life360/translations/et.json +++ b/homeassistant/components/life360/translations/et.json @@ -29,7 +29,7 @@ "username": "Kasutajanimi" }, "description": "T\u00e4psemate suvandite kohta leiad teemat [Life360 documentation]({docs_url}).\nTee seda enne uute kontode lisamist.", - "title": "Life360 konto teave" + "title": "Seadista Life360 konto" } } }, diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 5dbd2898971..33f615d00c7 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", @@ -23,5 +32,19 @@ "title": "Life360 fi\u00f3kadatok" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Vezet\u00e9s megjelen\u00edt\u00e9se \u00e1llapotk\u00e9nt", + "driving_speed": "Menetsebess\u00e9g", + "limit_gps_acc": "A GPS pontoss\u00e1g\u00e1nak korl\u00e1toz\u00e1sa", + "max_gps_accuracy": "Maxim\u00e1lis GPS pontoss\u00e1g (m\u00e9ter)", + "set_drive_speed": "Vezet\u00e9si sebess\u00e9g k\u00fcsz\u00f6b\u00e9rt\u00e9k\u00e9nek be\u00e1ll\u00edt\u00e1sa" + }, + "title": "Fi\u00f3kbe\u00e1ll\u00edt\u00e1sok" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/tr.json b/homeassistant/components/life360/translations/tr.json index 1304151d948..52b083b83fd 100644 --- a/homeassistant/components/life360/translations/tr.json +++ b/homeassistant/components/life360/translations/tr.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_username": "Ge\u00e7ersiz kullan\u0131c\u0131 ad\u0131", "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Geli\u015fmi\u015f se\u00e7enekleri ayarlamak i\u00e7in [Life360 belgelerine]( {docs_url} ) bak\u0131n.\n Bunu hesap eklemeden \u00f6nce yapmak isteyebilirsiniz.", - "title": "Life360 Hesap Bilgileri" + "title": "Life360 Hesab\u0131n\u0131 Yap\u0131land\u0131r" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "S\u00fcr\u00fc\u015f\u00fc durum olarak g\u00f6ster", + "driving_speed": "S\u00fcr\u00fc\u015f h\u0131z\u0131", + "limit_gps_acc": "GPS do\u011frulu\u011funu s\u0131n\u0131rlay\u0131n", + "max_gps_accuracy": "Maksimum GPS do\u011frulu\u011fu (metre)", + "set_drive_speed": "S\u00fcr\u00fc\u015f h\u0131z\u0131 e\u015fi\u011fini ayarla" + }, + "title": "Hesap Se\u00e7enekleri" } } } diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index ee9fcbba98d..9aff487997c 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -37,6 +37,7 @@ "data": { "cloud_project_id": "Google Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID" }, + "description": "\u4ee5\u4e0b\u306bCloud Project ID\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u4f8b: *example-project-12345*\u3002[Google Cloud Console]({cloud_console_url}) \u307e\u305f\u306f[\u8a73\u7d30]({more_info_url})\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30cd\u30b9\u30c8: \u30af\u30e9\u30a6\u30c9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID\u3092\u5165\u529b" }, "create_cloud_project": { diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index 1732443184e..bc69b928ba1 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -23,7 +23,7 @@ "subscriber_error": "Bilinmeyen abone hatas\u0131, g\u00fcnl\u00fcklere bak\u0131n\u0131z", "timeout": "Zaman a\u015f\u0131m\u0131 do\u011frulama kodu", "unknown": "Beklenmeyen hata", - "wrong_project_id": "L\u00fctfen ge\u00e7erli bir Bulut Projesi Kimli\u011fi girin (bulunan Ayg\u0131t Eri\u015fimi Proje Kimli\u011fi)" + "wrong_project_id": "L\u00fctfen ge\u00e7erli bir Bulut Projesi Kimli\u011fi girin (Cihaz Eri\u015fimi Proje Kimli\u011fi ile ayn\u0131yd\u0131)" }, "step": { "auth": { diff --git a/homeassistant/components/nina/translations/hu.json b/homeassistant/components/nina/translations/hu.json index 24a0d59cbae..73c76f45124 100644 --- a/homeassistant/components/nina/translations/hu.json +++ b/homeassistant/components/nina/translations/hu.json @@ -23,5 +23,27 @@ "title": "V\u00e1lasszon v\u00e1rost/megy\u00e9t" } } + }, + "options": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_selection": "K\u00e9rem, v\u00e1lasszon legal\u00e1bb egy v\u00e1rost/megy\u00e9t", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "init": { + "data": { + "_a_to_d": "V\u00e1ros/megye (A-D)", + "_e_to_h": "V\u00e1ros/megye (E-H)", + "_i_to_l": "V\u00e1ros/megye (I-L)", + "_m_to_q": "V\u00e1ros/megye (M-Q)", + "_r_to_u": "V\u00e1ros/megye (R-U)", + "_v_to_z": "V\u00e1ros/megye (V-Z)", + "corona_filter": "Corona figyelmeztet\u00e9sek bez\u00e1r\u00e1sa", + "slots": "A figyelmeztet\u00e9sek maxim\u00e1lis sz\u00e1ma v\u00e1rosonk\u00e9nt/megy\u00e9nk\u00e9nt" + }, + "title": "Be\u00e1ll\u00edt\u00e1sok" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/tr.json b/homeassistant/components/nina/translations/tr.json index 5fffe4c4f69..5311219271b 100644 --- a/homeassistant/components/nina/translations/tr.json +++ b/homeassistant/components/nina/translations/tr.json @@ -23,5 +23,27 @@ "title": "\u015eehir/il\u00e7e se\u00e7in" } } + }, + "options": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_selection": "L\u00fctfen en az bir \u015fehir/il\u00e7e se\u00e7in", + "unknown": "Beklenmeyen hata" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u015eehir/il\u00e7e (A-D)", + "_e_to_h": "\u015eehir/il\u00e7e (E-H)", + "_i_to_l": "\u015eehir/il\u00e7e (I-L)", + "_m_to_q": "\u015eehir/il\u00e7e (M-Q)", + "_r_to_u": "\u015eehir/il\u00e7e (R-U)", + "_v_to_z": "\u015eehir/il\u00e7e (V-Z)", + "corona_filter": "Korona Uyar\u0131lar\u0131n\u0131 Kald\u0131r", + "slots": "\u015eehir/il\u00e7e ba\u015f\u0131na maksimum uyar\u0131 say\u0131s\u0131" + }, + "title": "Se\u00e7enekler" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.tr.json b/homeassistant/components/overkiz/translations/sensor.tr.json index b04b5a793bc..b9e88b93d41 100644 --- a/homeassistant/components/overkiz/translations/sensor.tr.json +++ b/homeassistant/components/overkiz/translations/sensor.tr.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Temiz", "dirty": "Kirli" + }, + "overkiz__three_way_handle_direction": { + "closed": "Kapal\u0131", + "open": "A\u00e7\u0131k", + "tilt": "E\u011fim" } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index b4b49a5e52d..def3c6f8436 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -19,8 +19,8 @@ "port": "Port", "username": "Smile Kullan\u0131c\u0131 Ad\u0131" }, - "description": "\u00dcr\u00fcn:", - "title": "Plugwise tipi" + "description": "L\u00fctfen girin", + "title": "Smile'a Ba\u011flan\u0131n" }, "user_gateway": { "data": { diff --git a/homeassistant/components/qnap_qsw/translations/ca.json b/homeassistant/components/qnap_qsw/translations/ca.json index 575ed369b7b..6864ced54e2 100644 --- a/homeassistant/components/qnap_qsw/translations/ca.json +++ b/homeassistant/components/qnap_qsw/translations/ca.json @@ -9,6 +9,12 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { + "discovered_connection": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/qnap_qsw/translations/de.json b/homeassistant/components/qnap_qsw/translations/de.json index ad2599e24c4..a2b89d77ea7 100644 --- a/homeassistant/components/qnap_qsw/translations/de.json +++ b/homeassistant/components/qnap_qsw/translations/de.json @@ -9,6 +9,12 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "discovered_connection": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/qnap_qsw/translations/fr.json b/homeassistant/components/qnap_qsw/translations/fr.json index 2631bcdfc87..2c885fee397 100644 --- a/homeassistant/components/qnap_qsw/translations/fr.json +++ b/homeassistant/components/qnap_qsw/translations/fr.json @@ -9,6 +9,12 @@ "invalid_auth": "Authentification non valide" }, "step": { + "discovered_connection": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/qnap_qsw/translations/hu.json b/homeassistant/components/qnap_qsw/translations/hu.json index c41ba084d95..60eb41375c1 100644 --- a/homeassistant/components/qnap_qsw/translations/hu.json +++ b/homeassistant/components/qnap_qsw/translations/hu.json @@ -9,6 +9,12 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "discovered_connection": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/qnap_qsw/translations/ja.json b/homeassistant/components/qnap_qsw/translations/ja.json index 0658405fd3f..09b93bc58c5 100644 --- a/homeassistant/components/qnap_qsw/translations/ja.json +++ b/homeassistant/components/qnap_qsw/translations/ja.json @@ -9,6 +9,12 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { + "discovered_connection": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/qnap_qsw/translations/no.json b/homeassistant/components/qnap_qsw/translations/no.json index 9eff9c22080..97f04b406a5 100644 --- a/homeassistant/components/qnap_qsw/translations/no.json +++ b/homeassistant/components/qnap_qsw/translations/no.json @@ -9,6 +9,12 @@ "invalid_auth": "Ugyldig godkjenning" }, "step": { + "discovered_connection": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/qnap_qsw/translations/pt-BR.json b/homeassistant/components/qnap_qsw/translations/pt-BR.json index b9829d78944..fe2016bdce5 100644 --- a/homeassistant/components/qnap_qsw/translations/pt-BR.json +++ b/homeassistant/components/qnap_qsw/translations/pt-BR.json @@ -9,6 +9,12 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "discovered_connection": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + }, "user": { "data": { "password": "Senha", diff --git a/homeassistant/components/qnap_qsw/translations/tr.json b/homeassistant/components/qnap_qsw/translations/tr.json index 309f2da3a90..ce11f88deca 100644 --- a/homeassistant/components/qnap_qsw/translations/tr.json +++ b/homeassistant/components/qnap_qsw/translations/tr.json @@ -9,6 +9,12 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "discovered_connection": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/qnap_qsw/translations/zh-Hant.json b/homeassistant/components/qnap_qsw/translations/zh-Hant.json index f29c42d6eb6..1461a38c7d4 100644 --- a/homeassistant/components/qnap_qsw/translations/zh-Hant.json +++ b/homeassistant/components/qnap_qsw/translations/zh-Hant.json @@ -9,6 +9,12 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { + "discovered_connection": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/simplepush/translations/tr.json b/homeassistant/components/simplepush/translations/tr.json new file mode 100644 index 00000000000..0c969465da9 --- /dev/null +++ b/homeassistant/components/simplepush/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "device_key": "Cihaz\u0131n\u0131z\u0131n cihaz anahtar\u0131", + "event": "Olaylar i\u00e7in olay.", + "name": "Ad", + "password": "Cihaz\u0131n\u0131z taraf\u0131ndan kullan\u0131lan \u015fifreleme parolas\u0131", + "salt": "Cihaz\u0131n\u0131z taraf\u0131ndan kullan\u0131lan tuz." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ca.json b/homeassistant/components/soundtouch/translations/ca.json new file mode 100644 index 00000000000..baba19644fb --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + }, + "zeroconf_confirm": { + "description": "Est\u00e0s a punt d'afegir el dispositiu SoundTouch amb nom `{name}` a Home Assistant.", + "title": "Confirma l'addici\u00f3 del dispositiu Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/de.json b/homeassistant/components/soundtouch/translations/de.json new file mode 100644 index 00000000000..8d28b988834 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Du bist im Begriff, das SoundTouch-Ger\u00e4t mit dem Namen `{name}` zu Home Assistant hinzuzuf\u00fcgen.", + "title": "Best\u00e4tige das Hinzuf\u00fcgen des Bose SoundTouch-Ger\u00e4ts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/et.json b/homeassistant/components/soundtouch/translations/et.json new file mode 100644 index 00000000000..0adb9ddba8b --- /dev/null +++ b/homeassistant/components/soundtouch/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Oled lisamas SoundTouchi seadet nimega `{name}` Home Assistantisse.", + "title": "Kinnita Bose SoundTouchi seadme lisamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/fr.json b/homeassistant/components/soundtouch/translations/fr.json new file mode 100644 index 00000000000..e0e187756af --- /dev/null +++ b/homeassistant/components/soundtouch/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + }, + "zeroconf_confirm": { + "description": "Vous \u00eates sur le point d'ajouter l'appareil SoundTouch nomm\u00e9 `{name}` \u00e0 Home Assistant.", + "title": "Confirmer l'ajout de l'appareil Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/hu.json b/homeassistant/components/soundtouch/translations/hu.json new file mode 100644 index 00000000000..a0f5c364af6 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + }, + "zeroconf_confirm": { + "description": "Hamarosan hozz\u00e1adja a \"{name}\" nev\u0171 SoundTouch eszk\u00f6zt a Home Assistanthoz.", + "title": "Bose SoundTouch eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1nak meger\u0151s\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ja.json b/homeassistant/components/soundtouch/translations/ja.json new file mode 100644 index 00000000000..c4a94a23a7e --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + }, + "zeroconf_confirm": { + "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eSoundTouch\u30c7\u30d0\u30a4\u30b9\u3092\u3001Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002", + "title": "Bose SoundTouch\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/no.json b/homeassistant/components/soundtouch/translations/no.json new file mode 100644 index 00000000000..bdaee03da54 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + }, + "zeroconf_confirm": { + "description": "Du er i ferd med \u00e5 legge til SoundTouch-enheten med navnet ` {name} ` til Home Assistant.", + "title": "Bekreft \u00e5 legge til Bose SoundTouch-enhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt-BR.json b/homeassistant/components/soundtouch/translations/pt-BR.json new file mode 100644 index 00000000000..e707219f342 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Voc\u00ea est\u00e1 prestes a adicionar o dispositivo SoundTouch chamado ` {name} ` ao Home Assistant.", + "title": "Confirme a adi\u00e7\u00e3o do dispositivo Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/tr.json b/homeassistant/components/soundtouch/translations/tr.json new file mode 100644 index 00000000000..957215dd8b3 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + }, + "zeroconf_confirm": { + "description": "` {name} ` adl\u0131 SoundTouch cihaz\u0131n\u0131 Home Assistant'a eklemek \u00fczeresiniz.", + "title": "Bose SoundTouch cihaz\u0131 eklemeyi onaylay\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json new file mode 100644 index 00000000000..08231b8571d --- /dev/null +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + }, + "zeroconf_confirm": { + "description": "\u6b63\u8981\u65b0\u589e\u540d\u70ba `{name}` \u7684 SoundTouch \u88dd\u7f6e\u81f3 Home Assistant\u3002", + "title": "\u78ba\u8a8d\u65b0\u589e Bose SoundTouch \u88dd\u7f6e" + } + } + } +} \ No newline at end of file From 255c3c5b9a82639cf7807e76abb59657cb3569d1 Mon Sep 17 00:00:00 2001 From: Khole Date: Sat, 2 Jul 2022 13:33:10 +0100 Subject: [PATCH 067/820] Hive add entity categories to entities (#74324) add enitity categories to entities --- homeassistant/components/hive/sensor.py | 3 +++ homeassistant/components/hive/switch.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 5bac23fdb3d..d224ec085f3 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, POWER_KILO_WATT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity @@ -23,12 +24,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="Power", native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, ), ) diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 64a8276521a..f72f228c595 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -6,6 +6,7 @@ from datetime import timedelta from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity, refresh_system @@ -19,7 +20,10 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="activeplug", ), - SwitchEntityDescription(key="Heating_Heat_On_Demand"), + SwitchEntityDescription( + key="Heating_Heat_On_Demand", + entity_category=EntityCategory.CONFIG, + ), ) From da11cef29a7dc2bff65229cc3f2809e7344e4e3d Mon Sep 17 00:00:00 2001 From: atlflyer <31390797+atlflyer@users.noreply.github.com> Date: Sat, 2 Jul 2022 09:58:08 -0400 Subject: [PATCH 068/820] Report error code in log when command fails (#74319) --- homeassistant/components/command_line/__init__.py | 12 +++++++++--- homeassistant/components/command_line/cover.py | 7 +++++-- homeassistant/components/command_line/notify.py | 6 +++++- tests/components/command_line/test_binary_sensor.py | 13 +++++++++++++ tests/components/command_line/test_cover.py | 3 ++- tests/components/command_line/test_notify.py | 1 + tests/components/command_line/test_sensor.py | 11 +++++++++++ tests/components/command_line/test_switch.py | 13 +++++++++++++ 8 files changed, 59 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 1bcaaaa60a6..172c321c0ec 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -23,7 +23,11 @@ def call_shell_with_timeout( return 0 except subprocess.CalledProcessError as proc_exception: if log_return_code: - _LOGGER.error("Command failed: %s", command) + _LOGGER.error( + "Command failed (with return code %s): %s", + proc_exception.returncode, + command, + ) return proc_exception.returncode except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", command) @@ -40,8 +44,10 @@ def check_output_or_log(command: str, timeout: int) -> str | None: command, shell=True, timeout=timeout # nosec # shell by design ) return return_value.strip().decode("utf-8") - except subprocess.CalledProcessError: - _LOGGER.error("Command failed: %s", command) + except subprocess.CalledProcessError as err: + _LOGGER.error( + "Command failed (with return code %s): %s", err.returncode, command + ) except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", command) except subprocess.SubprocessError: diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 609166f2d16..8298201228f 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -115,10 +115,13 @@ class CommandCover(CoverEntity): """Execute the actual commands.""" _LOGGER.info("Running command: %s", command) - success = call_shell_with_timeout(command, self._timeout) == 0 + returncode = call_shell_with_timeout(command, self._timeout) + success = returncode == 0 if not success: - _LOGGER.error("Command failed: %s", command) + _LOGGER.error( + "Command failed (with return code %s): %s", returncode, command + ) return success diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 6f364947775..7bce5010d45 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -57,7 +57,11 @@ class CommandLineNotificationService(BaseNotificationService): try: proc.communicate(input=message, timeout=self._timeout) if proc.returncode != 0: - _LOGGER.error("Command failed: %s", self.command) + _LOGGER.error( + "Command failed (with return code %s): %s", + proc.returncode, + self.command, + ) except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", self.command) kill_subprocess(proc) diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index b523b02aa64..18f680fbb02 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from pytest import LogCaptureFixture + from homeassistant import setup from homeassistant.components.binary_sensor import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON @@ -112,3 +114,14 @@ async def test_unique_id(hass: HomeAssistant) -> None: ) is not None ) + + +async def test_return_code(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: + """Test setting the state with a template.""" + await setup_test_entity( + hass, + { + "command": "exit 33", + }, + ) + assert "return code 33" in caplog.text diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 98c917f51ba..deb80953428 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -155,7 +155,7 @@ async def test_reload(hass: HomeAssistant) -> None: async def test_move_cover_failure( caplog: LogCaptureFixture, hass: HomeAssistant ) -> None: - """Test with state value.""" + """Test command failure.""" await setup_test_entity( hass, @@ -165,6 +165,7 @@ async def test_move_cover_failure( DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True ) assert "Command failed" in caplog.text + assert "return code 1" in caplog.text async def test_unique_id(hass: HomeAssistant) -> None: diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 26b53a827e7..5cef13e45b4 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -77,6 +77,7 @@ async def test_error_for_none_zero_exit_code( DOMAIN, "test", {"message": "error"}, blocking=True ) assert "Command failed" in caplog.text + assert "return code 1" in caplog.text async def test_timeout(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 62ec1dbe97b..bdb36eebaa1 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -128,6 +128,17 @@ async def test_bad_command(hass: HomeAssistant) -> None: assert entity_state.state == "unknown" +async def test_return_code(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: + """Test that an error return code is logged.""" + await setup_test_entities( + hass, + { + "command": "exit 33", + }, + ) + assert "return code 33" in caplog.text + + async def test_update_with_json_attrs(hass: HomeAssistant) -> None: """Test attributes get extracted from a JSON result.""" await setup_test_entities( diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 307974ab3fe..267f7cf7b06 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -419,3 +419,16 @@ async def test_unique_id(hass: HomeAssistant) -> None: ent_reg.async_get_entity_id("switch", "command_line", "not-so-unique-anymore") is not None ) + + +async def test_command_failure(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: + """Test command failure.""" + + await setup_test_entity( + hass, + {"test": {"command_off": "exit 33"}}, + ) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True + ) + assert "return code 33" in caplog.text From 2464322dc5693b1a0dc979d5da4b4e4acf1fb573 Mon Sep 17 00:00:00 2001 From: Frank <46161394+BraveChicken1@users.noreply.github.com> Date: Sat, 2 Jul 2022 16:06:07 +0200 Subject: [PATCH 069/820] Address HomeConnect late review (#74308) * Small changes as requested in PR 58768 * Fix ValueError message formatting * Use f-string * Remove None as return type of _get_appliance_by_device_id Co-authored-by: Martin Hjelmare --- .../components/home_connect/__init__.py | 40 ++++++++----------- homeassistant/components/home_connect/api.py | 1 - .../components/home_connect/entity.py | 1 - 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index f57c7aeb8af..50e51b4ae1e 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -103,15 +103,14 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR, Platform.S def _get_appliance_by_device_id( hass: HomeAssistant, device_id: str -) -> api.HomeConnectDevice | None: +) -> api.HomeConnectDevice: """Return a Home Connect appliance instance given an device_id.""" for hc_api in hass.data[DOMAIN].values(): for dev_dict in hc_api.devices: device = dev_dict[CONF_DEVICE] if device.device_id == device_id: return device.appliance - _LOGGER.error("Appliance for device id %s not found", device_id) - return None + raise ValueError(f"Appliance for device id {device_id} not found") async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -148,18 +147,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: } appliance = _get_appliance_by_device_id(hass, device_id) - if appliance is not None: - await hass.async_add_executor_job( - getattr(appliance, method), program, options - ) + await hass.async_add_executor_job(getattr(appliance, method), program, options) async def _async_service_command(call, command): """Execute calls to services executing a command.""" device_id = call.data[ATTR_DEVICE_ID] appliance = _get_appliance_by_device_id(hass, device_id) - if appliance is not None: - await hass.async_add_executor_job(appliance.execute_command, command) + await hass.async_add_executor_job(appliance.execute_command, command) async def _async_service_key_value(call, method): """Execute calls to services taking a key and value.""" @@ -169,20 +164,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: device_id = call.data[ATTR_DEVICE_ID] appliance = _get_appliance_by_device_id(hass, device_id) - if appliance is not None: - if unit is not None: - await hass.async_add_executor_job( - getattr(appliance, method), - key, - value, - unit, - ) - else: - await hass.async_add_executor_job( - getattr(appliance, method), - key, - value, - ) + if unit is not None: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + unit, + ) + else: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + ) async def async_service_option_active(call): """Service for setting an option for an active program.""" diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index 00d759b47d5..f3c98e618b8 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -113,7 +113,6 @@ class HomeConnectDevice: """Initialize the device class.""" self.hass = hass self.appliance = appliance - self.entities = [] def initialize(self): """Fetch the info needed to initialize the device.""" diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index 60a0c3974cd..b27988f997d 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -20,7 +20,6 @@ class HomeConnectEntity(Entity): self.device = device self.desc = desc self._name = f"{self.device.appliance.name} {desc}" - self.device.entities.append(self) async def async_added_to_hass(self): """Register callbacks.""" From 6f67ae1dfcce397b52e2de3d180fea8df9906b4c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 2 Jul 2022 18:04:35 +0200 Subject: [PATCH 070/820] Add nightly frontend to nightly builds (#74327) --- .github/workflows/builder.yml | 24 ++++++++++++++++++++++++ Dockerfile | 7 +++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 6eea7cea953..870bfb9b1e9 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -102,6 +102,17 @@ jobs: - name: Checkout the repository uses: actions/checkout@v3.0.2 + - name: Download nightly wheels of frontend + if: needs.init.outputs.channel == 'dev' + uses: dawidd6/action-download-artifact@v2 + with: + github_token: ${{secrets.GITHUB_TOKEN}} + repo: home-assistant/frontend + branch: dev + workflow: nightly.yml + workflow_conclusion: success + name: wheels + - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' uses: actions/setup-python@v4.0.0 @@ -116,6 +127,19 @@ jobs: python3 -m pip install --use-deprecated=legacy-resolver . version="$(python3 script/version_bump.py nightly)" + if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then + echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}" + frontend_version="${BASH_REMATCH[1]}" yq \ + --inplace e -o json \ + '.requirements = ["home-assistant-frontend=="+env(frontend_version)]' \ + homeassistant/components/frontend/manifest.json + + sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \ + homeassistant/package_constraints.txt + + python -m script.gen_requirements_all + fi + - name: Write meta info file shell: bash run: | diff --git a/Dockerfile b/Dockerfile index 13552d55a3d..03bd9131ea0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,12 @@ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements.txt --use-deprecated=legacy-resolver -COPY requirements_all.txt homeassistant/ +COPY requirements_all.txt home_assistant_frontend-* homeassistant/ RUN \ - pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ + pip3 install --no-cache-dir --no-index homeassistant/home_assistant_frontend-*.whl; \ + fi \ + && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver ## Setup Home Assistant Core From 64bfa20f6a6fb44255763abe47c304b4227db162 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 2 Jul 2022 19:15:54 +0200 Subject: [PATCH 071/820] Improve type hints in mqtt (#74295) --- homeassistant/components/mqtt/climate.py | 56 ++++++++++----------- homeassistant/components/mqtt/humidifier.py | 10 ++-- homeassistant/components/mqtt/mixins.py | 4 +- homeassistant/components/mqtt/number.py | 5 +- homeassistant/components/mqtt/select.py | 2 +- homeassistant/components/mqtt/sensor.py | 2 +- homeassistant/components/mqtt/siren.py | 8 +-- homeassistant/components/mqtt/switch.py | 15 ++++-- 8 files changed, 53 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 6b09891483c..80ff33309ba 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations import functools import logging +from typing import Any import voluptuous as vol @@ -754,29 +755,29 @@ class MqttClimate(MqttEntity, ClimateEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self._config.get(CONF_TEMPERATURE_UNIT): - return self._config.get(CONF_TEMPERATURE_UNIT) + if unit := self._config.get(CONF_TEMPERATURE_UNIT): + return unit return self.hass.config.units.temperature_unit @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._current_temp @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._target_temp @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the low target temperature we try to reach.""" return self._target_temp_low @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the high target temperature we try to reach.""" return self._target_temp_high @@ -796,7 +797,7 @@ class MqttClimate(MqttEntity, ClimateEntity): return self._config[CONF_MODE_LIST] @property - def target_temperature_step(self): + def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" return self._config[CONF_TEMP_STEP] @@ -813,7 +814,7 @@ class MqttClimate(MqttEntity, ClimateEntity): return PRESET_NONE @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Return preset modes.""" presets = [] presets.extend(self._preset_modes) @@ -834,17 +835,17 @@ class MqttClimate(MqttEntity, ClimateEntity): return presets @property - def is_aux_heat(self): + def is_aux_heat(self) -> bool | None: """Return true if away mode is on.""" return self._aux @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the fan setting.""" return self._current_fan_mode @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return self._config[CONF_FAN_MODE_LIST] @@ -871,10 +872,9 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[cmnd_template](temp) await self._publish(cmnd_topic, payload) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" - if kwargs.get(ATTR_HVAC_MODE) is not None: - operation_mode = kwargs.get(ATTR_HVAC_MODE) + if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: await self.async_set_hvac_mode(operation_mode) await self._set_temperature( @@ -904,7 +904,7 @@ class MqttClimate(MqttEntity, ClimateEntity): # Always optimistic? self.async_write_ha_state() - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 if self._send_if_off or self._current_operation != HVACMode.OFF: @@ -917,7 +917,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._current_swing_mode = swing_mode self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target temperature.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 if self._send_if_off or self._current_operation != HVACMode.OFF: @@ -928,7 +928,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._current_fan_mode = fan_mode self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" if hvac_mode == HVACMode.OFF: await self._publish( @@ -945,12 +945,12 @@ class MqttClimate(MqttEntity, ClimateEntity): self.async_write_ha_state() @property - def swing_mode(self): + def swing_mode(self) -> str | None: """Return the swing setting.""" return self._current_swing_mode @property - def swing_modes(self): + def swing_modes(self) -> list[str]: """List of available swing modes.""" return self._config[CONF_SWING_MODE_LIST] @@ -1027,16 +1027,16 @@ class MqttClimate(MqttEntity, ClimateEntity): self._aux = state self.async_write_ha_state() - async def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" await self._set_aux_heat(True) - async def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" await self._set_aux_heat(False) @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" support = 0 @@ -1083,18 +1083,18 @@ class MqttClimate(MqttEntity, ClimateEntity): return support @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" return self._config[CONF_TEMP_MIN] @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self._config[CONF_TEMP_MAX] @property - def precision(self): + def precision(self) -> float: """Return the precision of the system.""" - if self._config.get(CONF_PRECISION) is not None: - return self._config.get(CONF_PRECISION) + if (precision := self._config.get(CONF_PRECISION)) is not None: + return precision return super().precision diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 5f09fc0d513..43ff2af65d4 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -3,6 +3,7 @@ from __future__ import annotations import functools import logging +from typing import Any import voluptuous as vol @@ -407,7 +408,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @@ -431,10 +432,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): """Return the current mode.""" return self._mode - async def async_turn_on( - self, - **kwargs, - ) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the entity. This method is a coroutine. @@ -451,7 +449,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the entity. This method is a coroutine. diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8e59d09dfce..8fe9ee564de 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -355,7 +355,7 @@ class MqttAttributes(Entity): def __init__(self, config: dict) -> None: """Initialize the JSON attributes mixin.""" - self._attributes: dict | None = None + self._attributes: dict[str, Any] | None = None self._attributes_sub_state = None self._attributes_config = config @@ -426,7 +426,7 @@ class MqttAttributes(Entity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index dc27a740720..9df9dbf818c 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -12,6 +12,7 @@ from homeassistant.components.number import ( DEFAULT_MIN_VALUE, DEFAULT_STEP, DEVICE_CLASSES_SCHEMA, + NumberDeviceClass, RestoreNumber, ) from homeassistant.config_entries import ConfigEntry @@ -292,11 +293,11 @@ class MqttNumber(MqttEntity, RestoreNumber): ) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def device_class(self) -> str | None: + def device_class(self) -> NumberDeviceClass | None: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 4c302446b19..bdf55f895f3 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -211,6 +211,6 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): ) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 6948e173039..0dda382bec4 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -347,7 +347,7 @@ class MqttSensor(MqttEntity, RestoreSensor): return self._config.get(CONF_UNIT_OF_MEASUREMENT) @property - def force_update(self): + def force_update(self) -> bool: """Force update.""" return self._config[CONF_FORCE_UPDATE] diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index dfb89d2ee79..f1de0d27de7 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -309,12 +309,12 @@ class MqttSiren(MqttEntity, SirenEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" mqtt_attributes = super().extra_state_attributes attributes = ( @@ -353,7 +353,7 @@ class MqttSiren(MqttEntity, SirenEntity): self._config[CONF_ENCODING], ) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the siren on. This method is a coroutine. @@ -371,7 +371,7 @@ class MqttSiren(MqttEntity, SirenEntity): self._update(kwargs) self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the siren off. This method is a coroutine. diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index b04f2433659..34009695257 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -2,11 +2,16 @@ from __future__ import annotations import functools +from typing import Any import voluptuous as vol from homeassistant.components import switch -from homeassistant.components.switch import DEVICE_CLASSES_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + DEVICE_CLASSES_SCHEMA, + SwitchDeviceClass, + SwitchEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -195,16 +200,16 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): return self._state @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def device_class(self) -> str | None: + def device_class(self) -> SwitchDeviceClass | None: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on. This method is a coroutine. @@ -221,7 +226,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off. This method is a coroutine. From d56a487169cb039f53da31bff8cda982c6ca3ff9 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 2 Jul 2022 21:20:40 +0300 Subject: [PATCH 072/820] Fix CI failure due to integrations leaving dirty known_devices.yaml (#74329) --- tests/components/demo/test_init.py | 13 +++++-------- tests/components/device_tracker/test_init.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index b00028e34d9..fa0aff8223b 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,12 +1,10 @@ """The tests for the Demo component.""" -from contextlib import suppress import json -import os +from unittest.mock import patch import pytest from homeassistant.components.demo import DOMAIN -from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.statistics import list_statistic_ids from homeassistant.helpers.json import JSONEncoder @@ -22,11 +20,10 @@ def mock_history(hass): @pytest.fixture(autouse=True) -def demo_cleanup(hass): - """Clean up device tracker demo file.""" - yield - with suppress(FileNotFoundError): - os.remove(hass.config.path(YAML_DEVICES)) +def mock_device_tracker_update_config(hass): + """Prevent device tracker from creating known devices file.""" + with patch("homeassistant.components.device_tracker.legacy.update_config"): + yield async def test_setting_up_demo(hass): diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 0953fc67b0a..d8914032f36 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -28,6 +28,8 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from . import common + from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -35,7 +37,6 @@ from tests.common import ( mock_restore_cache, patch_yaml_files, ) -from tests.components.device_tracker import common TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: "test"}} @@ -165,6 +166,7 @@ async def test_setup_without_yaml_file(hass, enable_custom_integrations): """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() async def test_gravatar(hass): @@ -210,10 +212,11 @@ async def test_gravatar_and_picture(hass): @patch("homeassistant.components.demo.device_tracker.setup_scanner", autospec=True) async def test_discover_platform(mock_demo_setup_scanner, mock_see, hass): """Test discovery of device_tracker demo platform.""" - await discovery.async_load_platform( - hass, device_tracker.DOMAIN, "demo", {"test_key": "test_val"}, {"bla": {}} - ) - await hass.async_block_till_done() + with patch("homeassistant.components.device_tracker.legacy.update_config"): + await discovery.async_load_platform( + hass, device_tracker.DOMAIN, "demo", {"test_key": "test_val"}, {"bla": {}} + ) + await hass.async_block_till_done() assert device_tracker.DOMAIN in hass.config.components assert mock_demo_setup_scanner.called assert mock_demo_setup_scanner.call_args[0] == ( From d9b326dd48189f67f6d6e72c092b2ea65b249afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 2 Jul 2022 21:44:20 +0200 Subject: [PATCH 073/820] Remove duplicated QNAP QSW format_mac call in config_flow (#74333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qnap_qsw: config_flow: remove duplicated format_mac Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index bb42c9ea294..794e8c67baa 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -78,7 +78,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("DHCP discovery detected QSW: %s", self._discovered_mac) - mac = format_mac(self._discovered_mac) options = ConnectionOptions(self._discovered_url, "", "") qsw = QnapQswApi(aiohttp_client.async_get_clientsession(self.hass), options) @@ -87,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except QswError as err: raise AbortFlow("cannot_connect") from err - await self.async_set_unique_id(format_mac(mac)) + await self.async_set_unique_id(format_mac(self._discovered_mac)) self._abort_if_unique_id_configured() return await self.async_step_discovered_connection() From c24c6b38b1121d4eff953bfe8a775c8c5c06d106 Mon Sep 17 00:00:00 2001 From: David Barshow Date: Sat, 2 Jul 2022 13:04:38 -0700 Subject: [PATCH 074/820] Support climate reproduce state fan_mode (#74317) climate reproduce state fan_mode support --- homeassistant/components/climate/reproduce_state.py | 5 +++++ tests/components/climate/test_reproduce_state.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index f7e63f475ea..0bbc6fce7ec 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -10,6 +10,7 @@ from homeassistant.core import Context, HomeAssistant, State from .const import ( ATTR_AUX_HEAT, + ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODE, ATTR_PRESET_MODE, @@ -19,6 +20,7 @@ from .const import ( DOMAIN, HVAC_MODES, SERVICE_SET_AUX_HEAT, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, @@ -70,6 +72,9 @@ async def _async_reproduce_states( if ATTR_SWING_MODE in state.attributes: await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE]) + if ATTR_FAN_MODE in state.attributes: + await call_service(SERVICE_SET_FAN_MODE, [ATTR_FAN_MODE]) + if ATTR_HUMIDITY in state.attributes: await call_service(SERVICE_SET_HUMIDITY, [ATTR_HUMIDITY]) diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index af1b14299ae..a6839043e62 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, + ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_PRESET_MODE, ATTR_SWING_MODE, @@ -14,6 +15,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, SERVICE_SET_AUX_HEAT, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, @@ -99,6 +101,7 @@ async def test_state_with_context(hass): (SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT), (SERVICE_SET_PRESET_MODE, ATTR_PRESET_MODE), (SERVICE_SET_SWING_MODE, ATTR_SWING_MODE), + (SERVICE_SET_FAN_MODE, ATTR_FAN_MODE), (SERVICE_SET_HUMIDITY, ATTR_HUMIDITY), (SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE), (SERVICE_SET_TEMPERATURE, ATTR_TARGET_TEMP_HIGH), From 3bebbf79700e5affb5b26021c3a64396a38b8032 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 2 Jul 2022 22:10:38 +0200 Subject: [PATCH 075/820] Add configuration directory to system health (#74318) --- homeassistant/components/homeassistant/strings.json | 3 ++- homeassistant/components/homeassistant/system_health.py | 1 + homeassistant/components/homeassistant/translations/en.json | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 43180b237b9..317e9d1dfcd 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -2,15 +2,16 @@ "system_health": { "info": { "arch": "CPU Architecture", + "config_dir": "Configuration Directory", "dev": "Development", "docker": "Docker", - "user": "User", "hassio": "Supervisor", "installation_type": "Installation Type", "os_name": "Operating System Family", "os_version": "Operating System Version", "python_version": "Python Version", "timezone": "Timezone", + "user": "User", "version": "Version", "virtualenv": "Virtual Environment" } diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index f13278ddfeb..4006228de25 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -29,4 +29,5 @@ async def system_health_info(hass): "os_version": info.get("os_version"), "arch": info.get("arch"), "timezone": info.get("timezone"), + "config_dir": hass.config.config_dir, } diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 977bc203fea..37c4498b32b 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU Architecture", + "config_dir": "Configuration Directory", "dev": "Development", "docker": "Docker", "hassio": "Supervisor", From 08c2bd82bd704a348c2b81cae4f73e2155cc728a Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 2 Jul 2022 21:27:47 +0100 Subject: [PATCH 076/820] Migrate metoffice to native_* (#74312) --- homeassistant/components/metoffice/weather.py | 25 ++++++++-------- tests/components/metoffice/test_weather.py | 29 +++++++++++-------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index daf37bcf83f..2e9a55b4415 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,15 +1,15 @@ """Support for UK Met Office weather service.""" from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -55,11 +55,11 @@ def _build_forecast_data(timestep): if timestep.precipitation: data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = timestep.precipitation.value if timestep.temperature: - data[ATTR_FORECAST_TEMP] = timestep.temperature.value + data[ATTR_FORECAST_NATIVE_TEMP] = timestep.temperature.value if timestep.wind_direction: data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value if timestep.wind_speed: - data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value + data[ATTR_FORECAST_NATIVE_WIND_SPEED] = timestep.wind_speed.value return data @@ -73,6 +73,10 @@ def _get_weather_condition(metoffice_code): class MetOfficeWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Office weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR + def __init__(self, coordinator, hass_data, use_3hourly): """Initialise the platform with a data instance.""" super().__init__(coordinator) @@ -94,17 +98,12 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return None @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" if self.coordinator.data.now.temperature: return self.coordinator.data.now.temperature.value return None - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def visibility(self): """Return the platform visibility.""" @@ -119,7 +118,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return _visibility @property - def pressure(self): + def native_pressure(self): """Return the mean sea-level pressure.""" weather_now = self.coordinator.data.now if weather_now and weather_now.pressure: @@ -135,7 +134,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return None @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_speed: diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index bf279ff3cf7..3b7ebd08678 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -133,7 +133,8 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -148,7 +149,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("forecast")[26]["condition"] == "cloudy" assert weather.attributes.get("forecast")[26]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[26]["temperature"] == 10 - assert weather.attributes.get("forecast")[26]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[26]["wind_speed"] == 6.44 assert weather.attributes.get("forecast")[26]["wind_bearing"] == "NNE" # Wavertree daily weather platform expected results @@ -157,7 +158,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -172,7 +173,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("forecast")[3]["condition"] == "rainy" assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" @@ -229,7 +230,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -244,7 +246,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[18]["condition"] == "clear-night" assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 1 assert weather.attributes.get("forecast")[18]["temperature"] == 9 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 6.44 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" # Wavertree daily weather platform expected results @@ -253,7 +255,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -268,7 +271,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[3]["condition"] == "rainy" assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" # King's Lynn 3-hourly weather platform expected results @@ -277,7 +280,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 14 - assert weather.attributes.get("wind_speed") == 2 + assert weather.attributes.get("wind_speed") == 3.22 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "E" assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 60 @@ -292,7 +296,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[18]["condition"] == "cloudy" assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[18]["temperature"] == 10 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 11.27 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" # King's Lynn daily weather platform expected results @@ -301,7 +305,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "cloudy" assert weather.attributes.get("temperature") == 9 - assert weather.attributes.get("wind_speed") == 4 + assert weather.attributes.get("wind_speed") == 6.44 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "ESE" assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 75 @@ -316,5 +321,5 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[2]["condition"] == "cloudy" assert weather.attributes.get("forecast")[2]["precipitation_probability"] == 14 assert weather.attributes.get("forecast")[2]["temperature"] == 11 - assert weather.attributes.get("forecast")[2]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[2]["wind_speed"] == 11.27 assert weather.attributes.get("forecast")[2]["wind_bearing"] == "ESE" From 98052646f2fc759bee72c4361f39679abd8fe9f0 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 2 Jul 2022 21:42:58 +0100 Subject: [PATCH 077/820] Remove visibility from metoffice weather (#74314) Co-authored-by: Paulus Schoutsen --- homeassistant/components/metoffice/weather.py | 15 --------------- tests/components/metoffice/test_weather.py | 6 ------ 2 files changed, 21 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 2e9a55b4415..f4e0bf61d30 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -27,8 +27,6 @@ from .const import ( MODE_3HOURLY_LABEL, MODE_DAILY, MODE_DAILY_LABEL, - VISIBILITY_CLASSES, - VISIBILITY_DISTANCE_CLASSES, ) @@ -104,19 +102,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.now.temperature.value return None - @property - def visibility(self): - """Return the platform visibility.""" - _visibility = None - weather_now = self.coordinator.data.now - if hasattr(weather_now, "visibility"): - visibility_class = VISIBILITY_CLASSES.get(weather_now.visibility.value) - visibility_distance = VISIBILITY_DISTANCE_CLASSES.get( - weather_now.visibility.value - ) - _visibility = f"{visibility_class} - {visibility_distance}" - return _visibility - @property def native_pressure(self): """Return the mean sea-level pressure.""" diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 3b7ebd08678..a93b1ea6b62 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -136,7 +136,6 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check @@ -160,7 +159,6 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("temperature") == 19 assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check @@ -233,7 +231,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check @@ -258,7 +255,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check @@ -283,7 +279,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 3.22 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "E" - assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 60 # Also has Forecast added - just pick out 1 entry to check @@ -308,7 +303,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 6.44 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "ESE" - assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 75 # All should have Forecast added - again, just picking out 1 entry to check From 8581db1da70349fa2fa2c531e8f6ac61fe898246 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 3 Jul 2022 00:26:52 +0000 Subject: [PATCH 078/820] [ci skip] Translation update --- .../components/anthemav/translations/el.json | 19 ++++++++++++++ .../components/anthemav/translations/it.json | 19 ++++++++++++++ .../components/esphome/translations/it.json | 4 +-- .../homeassistant/translations/fr.json | 1 + .../homeassistant/translations/it.json | 1 + .../homeassistant/translations/pt-BR.json | 1 + .../lg_soundbar/translations/el.json | 18 +++++++++++++ .../lg_soundbar/translations/it.json | 18 +++++++++++++ .../components/life360/translations/it.json | 25 ++++++++++++++++++- .../components/nina/translations/it.json | 22 ++++++++++++++++ .../components/qnap_qsw/translations/el.json | 6 +++++ .../components/qnap_qsw/translations/et.json | 6 +++++ .../components/qnap_qsw/translations/it.json | 6 +++++ .../components/qnap_qsw/translations/ko.json | 12 +++++++++ .../soundtouch/translations/el.json | 21 ++++++++++++++++ .../soundtouch/translations/it.json | 21 ++++++++++++++++ .../soundtouch/translations/ko.json | 11 ++++++++ 17 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/el.json create mode 100644 homeassistant/components/anthemav/translations/it.json create mode 100644 homeassistant/components/lg_soundbar/translations/el.json create mode 100644 homeassistant/components/lg_soundbar/translations/it.json create mode 100644 homeassistant/components/qnap_qsw/translations/ko.json create mode 100644 homeassistant/components/soundtouch/translations/el.json create mode 100644 homeassistant/components/soundtouch/translations/it.json create mode 100644 homeassistant/components/soundtouch/translations/ko.json diff --git a/homeassistant/components/anthemav/translations/el.json b/homeassistant/components/anthemav/translations/el.json new file mode 100644 index 00000000000..983e89155e8 --- /dev/null +++ b/homeassistant/components/anthemav/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "cannot_receive_deviceinfo": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 MAC. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json new file mode 100644 index 00000000000..12b0df56f0f --- /dev/null +++ b/homeassistant/components/anthemav/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "cannot_receive_deviceinfo": "Impossibile recuperare l'indirizzo MAC. Assicurati che il dispositivo sia acceso" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index 62c75238378..b1c4e12d088 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -9,7 +9,7 @@ "connection_error": "Impossibile connettersi a ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", "invalid_auth": "Autenticazione non valida", "invalid_psk": "La chiave di cifratura del trasporto non \u00e8 valida. Assicurati che corrisponda a ci\u00f2 che hai nella tua configurazione", - "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se questo errore persiste, imposta un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se l'errore persiste, impostare un indirizzo IP statico" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Porta" }, - "description": "Inserisci le impostazioni di connessione del tuo nodo [ESPHome](https://esphomelib.com/)." + "description": "Inserisci le impostazioni di connessione del tuo nodo [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/homeassistant/translations/fr.json b/homeassistant/components/homeassistant/translations/fr.json index ae9dfb0a7da..48e38978ba6 100644 --- a/homeassistant/components/homeassistant/translations/fr.json +++ b/homeassistant/components/homeassistant/translations/fr.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Architecture du processeur", + "config_dir": "R\u00e9pertoire de configuration", "dev": "D\u00e9veloppement", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index 3052a536338..432fb9dea46 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Architettura della CPU", + "config_dir": "Cartella di configurazione", "dev": "Sviluppo", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/pt-BR.json b/homeassistant/components/homeassistant/translations/pt-BR.json index f30a1775e0e..5ea540d67f3 100644 --- a/homeassistant/components/homeassistant/translations/pt-BR.json +++ b/homeassistant/components/homeassistant/translations/pt-BR.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arquitetura da CPU", + "config_dir": "Diret\u00f3rio de configura\u00e7\u00e3o", "dev": "Desenvolvimento", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/lg_soundbar/translations/el.json b/homeassistant/components/lg_soundbar/translations/el.json new file mode 100644 index 00000000000..7fa31f8fe9d --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "existing_instance_updated": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/it.json b/homeassistant/components/lg_soundbar/translations/it.json new file mode 100644 index 00000000000..c9f58f15aa2 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "existing_instance_updated": "Configurazione esistente aggiornata." + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/it.json b/homeassistant/components/life360/translations/it.json index 04d88e75378..4f139301274 100644 --- a/homeassistant/components/life360/translations/it.json +++ b/homeassistant/components/life360/translations/it.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", "invalid_auth": "Autenticazione non valida", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "unknown": "Errore imprevisto" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "invalid_username": "Nome utente non valido", "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", "username": "Nome utente" }, "description": "Per impostare le opzioni avanzate, vedere [Documentazione di Life360]({docs_url}).\n\u00c8 consigliabile eseguire questa operazione prima di aggiungere gli account.", - "title": "Informazioni sull'account Life360" + "title": "Configura l'account Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostra la guida come stato", + "driving_speed": "Velocit\u00e0 di guida", + "limit_gps_acc": "Limita la precisione del GPS", + "max_gps_accuracy": "Precisione GPS massima (metri)", + "set_drive_speed": "Impostare la soglia di velocit\u00e0 di guida" + }, + "title": "Opzioni dell'account" } } } diff --git a/homeassistant/components/nina/translations/it.json b/homeassistant/components/nina/translations/it.json index de54f8f9c1d..0e329cef6bd 100644 --- a/homeassistant/components/nina/translations/it.json +++ b/homeassistant/components/nina/translations/it.json @@ -23,5 +23,27 @@ "title": "Seleziona citt\u00e0/provincia" } } + }, + "options": { + "error": { + "cannot_connect": "Impossibile connettersi", + "no_selection": "Seleziona almeno una citt\u00e0/provincia", + "unknown": "Errore imprevisto" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Citt\u00e0/provincia (A-D)", + "_e_to_h": "Citt\u00e0/provincia (E-H)", + "_i_to_l": "Citt\u00e0/provincia (I-L)", + "_m_to_q": "Citt\u00e0/provincia (M-Q)", + "_r_to_u": "Citt\u00e0/provincia (R-U)", + "_v_to_z": "Citt\u00e0/provincia (V-Z)", + "corona_filter": "Rimuovi avvisi Corona", + "slots": "Avvisi massimi per citt\u00e0/provincia" + }, + "title": "Opzioni" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/el.json b/homeassistant/components/qnap_qsw/translations/el.json index 1bea5dcc9d7..5aeea0a6d22 100644 --- a/homeassistant/components/qnap_qsw/translations/el.json +++ b/homeassistant/components/qnap_qsw/translations/el.json @@ -9,6 +9,12 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "step": { + "discovered_connection": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/qnap_qsw/translations/et.json b/homeassistant/components/qnap_qsw/translations/et.json index eb7ddb6dbe7..7b43f0b00f8 100644 --- a/homeassistant/components/qnap_qsw/translations/et.json +++ b/homeassistant/components/qnap_qsw/translations/et.json @@ -9,6 +9,12 @@ "invalid_auth": "Tuvastamine nurjus" }, "step": { + "discovered_connection": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/qnap_qsw/translations/it.json b/homeassistant/components/qnap_qsw/translations/it.json index 0759ef4ca8a..557e937217b 100644 --- a/homeassistant/components/qnap_qsw/translations/it.json +++ b/homeassistant/components/qnap_qsw/translations/it.json @@ -9,6 +9,12 @@ "invalid_auth": "Autenticazione non valida" }, "step": { + "discovered_connection": { + "data": { + "password": "Password", + "username": "Nome utente" + } + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/qnap_qsw/translations/ko.json b/homeassistant/components/qnap_qsw/translations/ko.json new file mode 100644 index 00000000000..d31f3ea7fcd --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "discovered_connection": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/el.json b/homeassistant/components/soundtouch/translations/el.json new file mode 100644 index 00000000000..7346aeca660 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + }, + "zeroconf_confirm": { + "description": "\u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae SoundTouch \u03bc\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 `{name}` \u03c3\u03c4\u03bf Home Assistant.", + "title": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/it.json b/homeassistant/components/soundtouch/translations/it.json new file mode 100644 index 00000000000..a14492bf5c3 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Stai per aggiungere il dispositivo SoundTouch denominato `{name}` a Home Assistant.", + "title": "Conferma l'aggiunta del dispositivo Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ko.json b/homeassistant/components/soundtouch/translations/ko.json new file mode 100644 index 00000000000..fc4995bcf2c --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + } + } + } +} \ No newline at end of file From 820a782f94cafebe214fc558651fb78e9defc9ac Mon Sep 17 00:00:00 2001 From: shbatm Date: Sat, 2 Jul 2022 20:38:48 -0500 Subject: [PATCH 079/820] Onvif: bump onvif-zeep-async to 1.2.1 (#74341) * Update requirements_all.txt * Update requirements_test_all.txt * Update manifest.json --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index cd220500751..2df7c3004f0 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -2,7 +2,7 @@ "domain": "onvif", "name": "ONVIF", "documentation": "https://www.home-assistant.io/integrations/onvif", - "requirements": ["onvif-zeep-async==1.2.0", "WSDiscovery==2.0.0"], + "requirements": ["onvif-zeep-async==1.2.1", "WSDiscovery==2.0.0"], "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 75c6552de30..d71232f1fd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1150,7 +1150,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==1.2.0 +onvif-zeep-async==1.2.1 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d60f632108..c2a194ca559 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -797,7 +797,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==1.2.0 +onvif-zeep-async==1.2.1 # homeassistant.components.opengarage open-garage==0.2.0 From 5919082a53a710ee00a6eec901cdbc8bbb211580 Mon Sep 17 00:00:00 2001 From: mbo18 Date: Sun, 3 Jul 2022 16:47:06 +0200 Subject: [PATCH 080/820] Expose temperature and humidity sensors from AC (#74328) Expose temperature and humidity from AC --- homeassistant/components/smartthings/climate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 87d20b4533f..db13b5e6b37 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -106,9 +106,7 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: Capability.air_conditioner_mode, Capability.demand_response_load_control, Capability.air_conditioner_fan_mode, - Capability.relative_humidity_measurement, Capability.switch, - Capability.temperature_measurement, Capability.thermostat, Capability.thermostat_cooling_setpoint, Capability.thermostat_fan_mode, From a4c6cd2fbe3c3e65a5cfc39e2ffa7b3429d3ebc2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 3 Jul 2022 08:05:38 -0700 Subject: [PATCH 081/820] Cleanup Google Calendar unused test fixtures (#74353) --- tests/components/google/test_calendar.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 9a0cc2e47fa..f92519e2553 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -101,9 +101,7 @@ def upcoming_event_url(entity: str = TEST_ENTITY) -> str: return f"/api/calendars/{entity}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}" -async def test_all_day_event( - hass, mock_events_list_items, mock_token_read, component_setup -): +async def test_all_day_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" week_from_today = dt_util.now().date() + datetime.timedelta(days=7) end_event = week_from_today + datetime.timedelta(days=1) @@ -672,7 +670,6 @@ async def test_future_event_offset_update_behavior( async def test_unique_id( hass, mock_events_list_items, - mock_token_read, component_setup, config_entry, ): @@ -695,7 +692,6 @@ async def test_unique_id( async def test_unique_id_migration( hass, mock_events_list_items, - mock_token_read, component_setup, config_entry, old_unique_id, @@ -751,7 +747,6 @@ async def test_unique_id_migration( async def test_invalid_unique_id_cleanup( hass, mock_events_list_items, - mock_token_read, component_setup, config_entry, mock_calendars_yaml, From e7e940afa50cd050e679a9fd9d1b93b94fb68154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Sun, 3 Jul 2022 17:06:38 +0200 Subject: [PATCH 082/820] Address Blebox uniapi review sidenotes (#74298) * Changes accordingly to sidenotes given by @MartinHjelmare in pull #73834. * Mini version bump according to notes in pull #73834. * Error message fix, test adjustment. --- CODEOWNERS | 4 ++-- homeassistant/components/blebox/const.py | 21 +------------------ homeassistant/components/blebox/cover.py | 18 ++++++++++++++-- homeassistant/components/blebox/light.py | 9 ++++---- homeassistant/components/blebox/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/blebox/test_light.py | 19 +++++++++-------- 8 files changed, 37 insertions(+), 42 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0c3f73db76d..9e3c1e8e0b8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -131,8 +131,8 @@ build.json @home-assistant/supervisor /homeassistant/components/binary_sensor/ @home-assistant/core /tests/components/binary_sensor/ @home-assistant/core /homeassistant/components/bizkaibus/ @UgaitzEtxebarria -/homeassistant/components/blebox/ @bbx-a @bbx-jp @riokuu -/tests/components/blebox/ @bbx-a @bbx-jp @riokuu +/homeassistant/components/blebox/ @bbx-a @riokuu +/tests/components/blebox/ @bbx-a @riokuu /homeassistant/components/blink/ @fronzbot /tests/components/blink/ @fronzbot /homeassistant/components/blueprint/ @home-assistant/core diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index f0c6a2d7586..013a6501068 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -3,13 +3,7 @@ from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.switch import SwitchDeviceClass -from homeassistant.const import ( - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, - TEMP_CELSIUS, -) +from homeassistant.const import TEMP_CELSIUS DOMAIN = "blebox" PRODUCT = "product" @@ -30,19 +24,6 @@ BLEBOX_TO_HASS_DEVICE_CLASSES = { "temperature": SensorDeviceClass.TEMPERATURE, } -BLEBOX_TO_HASS_COVER_STATES = { - None: None, - 0: STATE_CLOSING, # moving down - 1: STATE_OPENING, # moving up - 2: STATE_OPEN, # manually stopped - 3: STATE_CLOSED, # lower limit - 4: STATE_OPEN, # upper limit / open - # gateController - 5: STATE_OPEN, # overload - 6: STATE_OPEN, # motor failure - # 7 is not used - 8: STATE_OPEN, # safety stop -} BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 368443988a4..8763ec34d34 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -9,12 +9,26 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_COVER_STATES, BLEBOX_TO_HASS_DEVICE_CLASSES +from .const import BLEBOX_TO_HASS_DEVICE_CLASSES + +BLEBOX_TO_HASS_COVER_STATES = { + None: None, + 0: STATE_CLOSING, # moving down + 1: STATE_OPENING, # moving up + 2: STATE_OPEN, # manually stopped + 3: STATE_CLOSED, # lower limit + 4: STATE_OPEN, # upper limit / open + # gateController + 5: STATE_OPEN, # overload + 6: STATE_OPEN, # motor failure + # 7 is not used + 8: STATE_OPEN, # safety stop +} async def async_setup_entry( diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 32cc6360db1..2bb6bc91762 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging -from blebox_uniapi.error import BadOnValueError import blebox_uniapi.light from blebox_uniapi.light import BleboxColorMode @@ -166,10 +165,10 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): else: try: await self._feature.async_on(value) - except BadOnValueError as ex: - _LOGGER.error( - "Turning on '%s' failed: Bad value %s (%s)", self.name, value, ex - ) + except ValueError as exc: + raise ValueError( + f"Turning on '{self.name}' failed: Bad value {value}" + ) from exc async def async_turn_off(self, **kwargs): """Turn the light off.""" diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 5c57d5f6b9f..08554695316 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,8 +3,8 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.0.0"], - "codeowners": ["@bbx-a", "@bbx-jp", "@riokuu"], + "requirements": ["blebox_uniapi==2.0.1"], + "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] } diff --git a/requirements_all.txt b/requirements_all.txt index d71232f1fd5..9468e937b57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -402,7 +402,7 @@ bimmer_connected==0.9.6 bizkaibus==0.1.1 # homeassistant.components.blebox -blebox_uniapi==2.0.0 +blebox_uniapi==2.0.1 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2a194ca559..c168bb44ce5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -317,7 +317,7 @@ bellows==0.31.0 bimmer_connected==0.9.6 # homeassistant.components.blebox -blebox_uniapi==2.0.0 +blebox_uniapi==2.0.1 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 4663c216136..f61496714fb 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -509,17 +509,18 @@ async def test_turn_on_failure(feature, hass, config, caplog): caplog.set_level(logging.ERROR) feature_mock, entity_id = feature - feature_mock.async_on = AsyncMock(side_effect=blebox_uniapi.error.BadOnValueError) + feature_mock.async_on = AsyncMock(side_effect=ValueError) await async_setup_entity(hass, config, entity_id) feature_mock.sensible_on_value = 123 - await hass.services.async_call( - "light", - SERVICE_TURN_ON, - {"entity_id": entity_id}, - blocking=True, - ) + with pytest.raises(ValueError) as info: + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, + ) - assert ( - f"Turning on '{feature_mock.full_name}' failed: Bad value 123 ()" in caplog.text + assert f"Turning on '{feature_mock.full_name}' failed: Bad value 123" in str( + info.value ) From 84119eefaac50d827ee184ecd79fb9e7d0f6f49b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 3 Jul 2022 18:51:50 +0200 Subject: [PATCH 083/820] Add NextDNS integration (#74150) * Initial commit * Update manifest * Add first test * Simplify init * More tests * Update tests * More tests * More tests * Add tests for sensor platform * More tests for sensor platform * Add tests for system_health * Fix typo * Improve test coverage * Improve test coverage * Add tests for diagnostics * Add comment * Run hassfest * Fix typo * Run gen_requirements_all * Fix tests * Change key name in diagnostics * Remove diagnostics and system_health platforms * Bump library --- CODEOWNERS | 2 + homeassistant/components/nextdns/__init__.py | 185 +++++++ .../components/nextdns/config_flow.py | 88 +++ homeassistant/components/nextdns/const.py | 15 + .../components/nextdns/manifest.json | 10 + homeassistant/components/nextdns/sensor.py | 299 +++++++++++ homeassistant/components/nextdns/strings.json | 24 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nextdns/__init__.py | 63 +++ tests/components/nextdns/test_config_flow.py | 100 ++++ tests/components/nextdns/test_init.py | 47 ++ tests/components/nextdns/test_sensor.py | 502 ++++++++++++++++++ 14 files changed, 1342 insertions(+) create mode 100644 homeassistant/components/nextdns/__init__.py create mode 100644 homeassistant/components/nextdns/config_flow.py create mode 100644 homeassistant/components/nextdns/const.py create mode 100644 homeassistant/components/nextdns/manifest.json create mode 100644 homeassistant/components/nextdns/sensor.py create mode 100644 homeassistant/components/nextdns/strings.json create mode 100644 tests/components/nextdns/__init__.py create mode 100644 tests/components/nextdns/test_config_flow.py create mode 100644 tests/components/nextdns/test_init.py create mode 100644 tests/components/nextdns/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 9e3c1e8e0b8..6ae1c9605db 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -698,6 +698,8 @@ build.json @home-assistant/supervisor /homeassistant/components/nextbus/ @vividboarder /tests/components/nextbus/ @vividboarder /homeassistant/components/nextcloud/ @meichthys +/homeassistant/components/nextdns/ @bieniu +/tests/components/nextdns/ @bieniu /homeassistant/components/nfandroidtv/ @tkdrob /tests/components/nfandroidtv/ @tkdrob /homeassistant/components/nightscout/ @marciogranzotto diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py new file mode 100644 index 00000000000..71ddeb9e8eb --- /dev/null +++ b/homeassistant/components/nextdns/__init__.py @@ -0,0 +1,185 @@ +"""The NextDNS component.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, + ApiError, + InvalidApiKeyError, + NextDns, +) +from nextdns.model import NextDnsData + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_DNSSEC, + ATTR_ENCRYPTION, + ATTR_IP_VERSIONS, + ATTR_PROTOCOLS, + ATTR_STATUS, + CONF_PROFILE_ID, + DOMAIN, + UPDATE_INTERVAL_ANALYTICS, +) + + +class NextDnsUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching NextDNS data API.""" + + def __init__( + self, + hass: HomeAssistant, + nextdns: NextDns, + profile_id: str, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.nextdns = nextdns + self.profile_id = profile_id + self.profile_name = nextdns.get_profile_name(profile_id) + self.device_info = DeviceInfo( + configuration_url=f"https://my.nextdns.io/{profile_id}/setup", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, str(profile_id))}, + manufacturer="NextDNS Inc.", + name=self.profile_name, + ) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self) -> NextDnsData: + """Update data via library.""" + raise NotImplementedError("Update method not implemented") + + +class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics status data from API.""" + + async def _async_update_data(self) -> AnalyticsStatus: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_status(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics Dnssec data from API.""" + + async def _async_update_data(self) -> AnalyticsDnssec: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_dnssec(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics encryption data from API.""" + + async def _async_update_data(self) -> AnalyticsEncryption: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_encryption(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics IP versions data from API.""" + + async def _async_update_data(self) -> AnalyticsIpVersions: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_ip_versions(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics protocols data from API.""" + + async def _async_update_data(self) -> AnalyticsProtocols: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_protocols(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] +COORDINATORS = [ + (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_IP_VERSIONS, NextDnsIpVersionsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_PROTOCOLS, NextDnsProtocolsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_STATUS, NextDnsStatusUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up NextDNS as config entry.""" + api_key = entry.data[CONF_API_KEY] + profile_id = entry.data[CONF_PROFILE_ID] + + websession = async_get_clientsession(hass) + try: + async with timeout(10): + nextdns = await NextDns.create(websession, api_key) + except (ApiError, ClientConnectorError, asyncio.TimeoutError) as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault(entry.entry_id, {}) + + tasks = [] + + # Independent DataUpdateCoordinator is used for each API endpoint to avoid + # unnecessary requests when entities using this endpoint are disabled. + for coordinator_name, coordinator_class, update_interval in COORDINATORS: + hass.data[DOMAIN][entry.entry_id][coordinator_name] = coordinator_class( + hass, nextdns, profile_id, update_interval + ) + tasks.append( + hass.data[DOMAIN][entry.entry_id][coordinator_name].async_refresh() + ) + + await asyncio.gather(*tasks) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok: bool = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py new file mode 100644 index 00000000000..c621accfe81 --- /dev/null +++ b/homeassistant/components/nextdns/config_flow.py @@ -0,0 +1,88 @@ +"""Adds config flow for NextDNS.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from nextdns import ApiError, InvalidApiKeyError, NextDns +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_PROFILE_ID, CONF_PROFILE_NAME, DOMAIN + + +class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for NextDNS.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self.nextdns: NextDns + self.api_key: str + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] = {} + + websession = async_get_clientsession(self.hass) + + if user_input is not None: + self.api_key = user_input[CONF_API_KEY] + try: + async with timeout(10): + self.nextdns = await NextDns.create( + websession, user_input[CONF_API_KEY] + ) + except InvalidApiKeyError: + errors["base"] = "invalid_api_key" + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + else: + return await self.async_step_profiles() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) + + async def async_step_profiles( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the profiles step.""" + errors: dict[str, str] = {} + + if user_input is not None: + profile_name = user_input[CONF_PROFILE_NAME] + profile_id = self.nextdns.get_profile_id(profile_name) + + await self.async_set_unique_id(profile_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=profile_name, + data={CONF_PROFILE_ID: profile_id, CONF_API_KEY: self.api_key}, + ) + + return self.async_show_form( + step_id="profiles", + data_schema=vol.Schema( + { + vol.Required(CONF_PROFILE_NAME): vol.In( + [profile.name for profile in self.nextdns.profiles] + ) + } + ), + errors=errors, + ) diff --git a/homeassistant/components/nextdns/const.py b/homeassistant/components/nextdns/const.py new file mode 100644 index 00000000000..04bab44354b --- /dev/null +++ b/homeassistant/components/nextdns/const.py @@ -0,0 +1,15 @@ +"""Constants for NextDNS integration.""" +from datetime import timedelta + +ATTR_DNSSEC = "dnssec" +ATTR_ENCRYPTION = "encryption" +ATTR_IP_VERSIONS = "ip_versions" +ATTR_PROTOCOLS = "protocols" +ATTR_STATUS = "status" + +CONF_PROFILE_ID = "profile_id" +CONF_PROFILE_NAME = "profile_name" + +UPDATE_INTERVAL_ANALYTICS = timedelta(minutes=10) + +DOMAIN = "nextdns" diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json new file mode 100644 index 00000000000..fd3dd46f846 --- /dev/null +++ b/homeassistant/components/nextdns/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "nextdns", + "name": "NextDNS", + "documentation": "https://www.home-assistant.io/integrations/nextdns", + "codeowners": ["@bieniu"], + "requirements": ["nextdns==1.0.0"], + "config_flow": true, + "iot_class": "cloud_polling", + "loggers": ["nextdns"] +} diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py new file mode 100644 index 00000000000..35a15f010b3 --- /dev/null +++ b/homeassistant/components/nextdns/sensor.py @@ -0,0 +1,299 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsUpdateCoordinator +from .const import ( + ATTR_DNSSEC, + ATTR_ENCRYPTION, + ATTR_IP_VERSIONS, + ATTR_PROTOCOLS, + ATTR_STATUS, + DOMAIN, +) + +PARALLEL_UPDATES = 1 + + +@dataclass +class NextDnsSensorRequiredKeysMixin: + """Class for NextDNS entity required keys.""" + + coordinator_type: str + + +@dataclass +class NextDnsSensorEntityDescription( + SensorEntityDescription, NextDnsSensorRequiredKeysMixin +): + """NextDNS sensor entity description.""" + + +SENSORS = ( + NextDnsSensorEntityDescription( + key="all_queries", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="{profile_name} DNS Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="blocked_queries", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="{profile_name} DNS Queries Blocked", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="relayed_queries", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="{profile_name} DNS Queries Relayed", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="blocked_queries_ratio", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="{profile_name} DNS Queries Blocked Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="doh_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} DNS-over-HTTPS Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="dot_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} DNS-over-TLS Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="doq_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} DNS-over-QUIC Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="udp_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} UDP Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="doh_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_registry_enabled_default=False, + icon="mdi:dns", + entity_category=EntityCategory.DIAGNOSTIC, + name="{profile_name} DNS-over-HTTPS Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="dot_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} DNS-over-TLS Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="doq_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_registry_enabled_default=False, + icon="mdi:dns", + entity_category=EntityCategory.DIAGNOSTIC, + name="{profile_name} DNS-over-QUIC Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="udp_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} UDP Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="encrypted_queries", + coordinator_type=ATTR_ENCRYPTION, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock", + name="{profile_name} Encrypted Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="unencrypted_queries", + coordinator_type=ATTR_ENCRYPTION, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-open", + name="{profile_name} Unencrypted Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="encrypted_queries_ratio", + coordinator_type=ATTR_ENCRYPTION, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock", + name="{profile_name} Encrypted Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="ipv4_queries", + coordinator_type=ATTR_IP_VERSIONS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:ip", + name="{profile_name} IPv4 Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="ipv6_queries", + coordinator_type=ATTR_IP_VERSIONS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:ip", + name="{profile_name} IPv6 Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="ipv6_queries_ratio", + coordinator_type=ATTR_IP_VERSIONS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:ip", + name="{profile_name} IPv6 Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="validated_queries", + coordinator_type=ATTR_DNSSEC, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-check", + name="{profile_name} DNSSEC Validated Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="not_validated_queries", + coordinator_type=ATTR_DNSSEC, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-alert", + name="{profile_name} DNSSEC Not Validated Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="validated_queries_ratio", + coordinator_type=ATTR_DNSSEC, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-check", + name="{profile_name} DNSSEC Validated Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add a NextDNS entities from a config_entry.""" + sensors: list[NextDnsSensor] = [] + coordinators = hass.data[DOMAIN][entry.entry_id] + + for description in SENSORS: + sensors.append( + NextDnsSensor(coordinators[description.coordinator_type], description) + ) + + async_add_entities(sensors) + + +class NextDnsSensor(CoordinatorEntity, SensorEntity): + """Define an NextDNS sensor.""" + + coordinator: NextDnsUpdateCoordinator + + def __init__( + self, + coordinator: NextDnsUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_name = cast(str, description.name).format( + profile_name=coordinator.profile_name + ) + self._attr_native_value = getattr(coordinator.data, description.key) + self.entity_description = description + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = getattr( + self.coordinator.data, self.entity_description.key + ) + self.async_write_ha_state() diff --git a/homeassistant/components/nextdns/strings.json b/homeassistant/components/nextdns/strings.json new file mode 100644 index 00000000000..db3cf88cf39 --- /dev/null +++ b/homeassistant/components/nextdns/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "profiles": { + "data": { + "profile": "Profile" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "This NextDNS profile is already configured." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index eba8bfe10c4..d3be2c5e674 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -235,6 +235,7 @@ FLOWS = { "netatmo", "netgear", "nexia", + "nextdns", "nfandroidtv", "nightscout", "nina", diff --git a/requirements_all.txt b/requirements_all.txt index 9468e937b57..87123abeba7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1094,6 +1094,9 @@ nextcloudmonitor==1.1.0 # homeassistant.components.discord nextcord==2.0.0a8 +# homeassistant.components.nextdns +nextdns==1.0.0 + # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c168bb44ce5..b47aefebf16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,6 +762,9 @@ nexia==2.0.1 # homeassistant.components.discord nextcord==2.0.0a8 +# homeassistant.components.nextdns +nextdns==1.0.0 + # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py new file mode 100644 index 00000000000..24063d957d4 --- /dev/null +++ b/tests/components/nextdns/__init__.py @@ -0,0 +1,63 @@ +"""Tests for the NextDNS integration.""" +from unittest.mock import patch + +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, +) + +from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN +from homeassistant.const import CONF_API_KEY + +from tests.common import MockConfigEntry + +PROFILES = [{"id": "xyz12", "fingerprint": "aabbccdd123", "name": "Fake Profile"}] +STATUS = AnalyticsStatus( + default_queries=40, allowed_queries=30, blocked_queries=20, relayed_queries=10 +) +DNSSEC = AnalyticsDnssec(not_validated_queries=25, validated_queries=75) +ENCRYPTION = AnalyticsEncryption(encrypted_queries=60, unencrypted_queries=40) +IP_VERSIONS = AnalyticsIpVersions(ipv4_queries=90, ipv6_queries=10) +PROTOCOLS = AnalyticsProtocols( + doh_queries=20, doq_queries=10, dot_queries=30, udp_queries=40 +) + + +async def init_integration(hass, add_to_hass=True) -> MockConfigEntry: + """Set up the NextDNS integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Fake Profile", + unique_id="xyz12", + data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + ) + + if not add_to_hass: + return entry + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_status", + return_value=STATUS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_encryption", + return_value=ENCRYPTION, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", + return_value=DNSSEC, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", + return_value=IP_VERSIONS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_protocols", + return_value=PROTOCOLS, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py new file mode 100644 index 00000000000..0b3ac8a798e --- /dev/null +++ b/tests/components/nextdns/test_config_flow.py @@ -0,0 +1,100 @@ +"""Define tests for the NextDNS config flow.""" +import asyncio +from unittest.mock import patch + +from nextdns import ApiError, InvalidApiKeyError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.nextdns.const import ( + CONF_PROFILE_ID, + CONF_PROFILE_NAME, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY + +from . import PROFILES, init_integration + + +async def test_form_create_entry(hass): + """Test that the user step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES + ), patch( + "homeassistant.components.nextdns.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "fake_api_key"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "profiles" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Fake Profile" + assert result["data"][CONF_API_KEY] == "fake_api_key" + assert result["data"][CONF_PROFILE_ID] == "xyz12" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "error", + [ + (ApiError("API Error"), "cannot_connect"), + (InvalidApiKeyError, "invalid_api_key"), + (asyncio.TimeoutError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_errors(hass, error): + """Test we handle errors.""" + exc, base_error = error + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", side_effect=exc + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_API_KEY: "fake_api_key"}, + ) + + assert result["errors"] == {"base": base_error} + + +async def test_form_already_configured(hass): + """Test that errors are shown when duplicates are added.""" + entry = await init_integration(hass) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES + ): + await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "fake_api_key"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py new file mode 100644 index 00000000000..c16fad4e812 --- /dev/null +++ b/tests/components/nextdns/test_init.py @@ -0,0 +1,47 @@ +"""Test init of NextDNS integration.""" +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNAVAILABLE + +from . import init_integration + + +async def test_async_setup_entry(hass): + """Test a successful setup entry.""" + await init_integration(hass) + + state = hass.states.get("sensor.fake_profile_dns_queries_blocked_ratio") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "20.0" + + +async def test_config_not_ready(hass): + """Test for setup failure if the connection to the service fails.""" + entry = await init_integration(hass, add_to_hass=False) + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", + side_effect=ApiError("API Error"), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py new file mode 100644 index 00000000000..fef2abafc85 --- /dev/null +++ b/tests/components/nextdns/test_sensor.py @@ -0,0 +1,502 @@ +"""Test sensor of NextDNS integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorStateClass, +) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import DNSSEC, ENCRYPTION, IP_VERSIONS, PROTOCOLS, STATUS, init_integration + +from tests.common import async_fire_time_changed + + +async def test_sensor(hass): + """Test states of sensors.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh_queries", + suggested_object_id="fake_profile_dns_over_https_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh_queries_ratio", + suggested_object_id="fake_profile_dns_over_https_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doq_queries", + suggested_object_id="fake_profile_dns_over_quic_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doq_queries_ratio", + suggested_object_id="fake_profile_dns_over_quic_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_dot_queries", + suggested_object_id="fake_profile_dns_over_tls_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_dot_queries_ratio", + suggested_object_id="fake_profile_dns_over_tls_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_not_validated_queries", + suggested_object_id="fake_profile_dnssec_not_validated_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_validated_queries", + suggested_object_id="fake_profile_dnssec_validated_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_validated_queries_ratio", + suggested_object_id="fake_profile_dnssec_validated_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_encrypted_queries", + suggested_object_id="fake_profile_encrypted_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_encrypted_queries_ratio", + suggested_object_id="fake_profile_encrypted_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv4_queries", + suggested_object_id="fake_profile_ipv4_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv6_queries", + suggested_object_id="fake_profile_ipv6_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv6_queries_ratio", + suggested_object_id="fake_profile_ipv6_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_udp_queries", + suggested_object_id="fake_profile_udp_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_udp_queries_ratio", + suggested_object_id="fake_profile_udp_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_unencrypted_queries", + suggested_object_id="fake_profile_unencrypted_queries", + disabled_by=None, + ) + + await init_integration(hass) + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state == "100" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_queries") + assert entry + assert entry.unique_id == "xyz12_all_queries" + + state = hass.states.get("sensor.fake_profile_dns_queries_blocked") + assert state + assert state.state == "20" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_queries_blocked") + assert entry + assert entry.unique_id == "xyz12_blocked_queries" + + state = hass.states.get("sensor.fake_profile_dns_queries_blocked_ratio") + assert state + assert state.state == "20.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_queries_blocked_ratio") + assert entry + assert entry.unique_id == "xyz12_blocked_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dns_queries_relayed") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_queries_relayed") + assert entry + assert entry.unique_id == "xyz12_relayed_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state == "20" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_https_queries") + assert entry + assert entry.unique_id == "xyz12_doh_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries_ratio") + assert state + assert state.state == "22.2" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_https_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_doh_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dns_over_quic_queries") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries") + assert entry + assert entry.unique_id == "xyz12_doq_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_quic_queries_ratio") + assert state + assert state.state == "11.1" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_doq_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dns_over_tls_queries") + assert state + assert state.state == "30" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries") + assert entry + assert entry.unique_id == "xyz12_dot_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_tls_queries_ratio") + assert state + assert state.state == "33.3" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_dot_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dnssec_not_validated_queries") + assert state + assert state.state == "25" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dnssec_not_validated_queries") + assert entry + assert entry.unique_id == "xyz12_not_validated_queries" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state == "75" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries") + assert entry + assert entry.unique_id == "xyz12_validated_queries" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries_ratio") + assert state + assert state.state == "75.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_validated_queries_ratio" + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state == "60" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_encrypted_queries") + assert entry + assert entry.unique_id == "xyz12_encrypted_queries" + + state = hass.states.get("sensor.fake_profile_unencrypted_queries") + assert state + assert state.state == "40" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_unencrypted_queries") + assert entry + assert entry.unique_id == "xyz12_unencrypted_queries" + + state = hass.states.get("sensor.fake_profile_encrypted_queries_ratio") + assert state + assert state.state == "60.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_encrypted_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_encrypted_queries_ratio" + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state == "90" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_ipv4_queries") + assert entry + assert entry.unique_id == "xyz12_ipv4_queries" + + state = hass.states.get("sensor.fake_profile_ipv6_queries") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_ipv6_queries") + assert entry + assert entry.unique_id == "xyz12_ipv6_queries" + + state = hass.states.get("sensor.fake_profile_ipv6_queries_ratio") + assert state + assert state.state == "10.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_ipv6_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_ipv6_queries_ratio" + + state = hass.states.get("sensor.fake_profile_udp_queries") + assert state + assert state.state == "40" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_udp_queries") + assert entry + assert entry.unique_id == "xyz12_udp_queries" + + state = hass.states.get("sensor.fake_profile_udp_queries_ratio") + assert state + assert state.state == "44.4" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_udp_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_udp_queries_ratio" + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh_queries", + suggested_object_id="fake_profile_dns_over_https_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_validated_queries", + suggested_object_id="fake_profile_dnssec_validated_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_encrypted_queries", + suggested_object_id="fake_profile_encrypted_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv4_queries", + suggested_object_id="fake_profile_ipv4_queries", + disabled_by=None, + ) + + await init_integration(hass) + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "100" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "20" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "75" + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "60" + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "90" + + future = utcnow() + timedelta(minutes=10) + with patch( + "homeassistant.components.nextdns.NextDns.get_analytics_status", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_encryption", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_protocols", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=20) + with patch( + "homeassistant.components.nextdns.NextDns.get_analytics_status", + return_value=STATUS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_encryption", + return_value=ENCRYPTION, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", + return_value=DNSSEC, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", + return_value=IP_VERSIONS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_protocols", + return_value=PROTOCOLS, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "100" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "20" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "75" + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "60" + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "90" From 936b271448c43cd19b1a0cce836ba494fe7b7b06 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 3 Jul 2022 22:06:50 +0200 Subject: [PATCH 084/820] Fix typo in nightly build (#74363) --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 870bfb9b1e9..860d11c62bf 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -109,7 +109,7 @@ jobs: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/frontend branch: dev - workflow: nightly.yml + workflow: nightly.yaml workflow_conclusion: success name: wheels From 57114c1a558d8e861a6c994606805f02145d1e20 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 3 Jul 2022 22:16:29 +0200 Subject: [PATCH 085/820] Add tomli as nightly build dependency (#74364) --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 860d11c62bf..826af2eb9fe 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -123,7 +123,7 @@ jobs: if: needs.init.outputs.channel == 'dev' shell: bash run: | - python3 -m pip install packaging + python3 -m pip install packaging tomli python3 -m pip install --use-deprecated=legacy-resolver . version="$(python3 script/version_bump.py nightly)" From 737a1fd9fa4b36c37ad07624adf180a2438826f8 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 3 Jul 2022 21:26:00 +0100 Subject: [PATCH 086/820] Dont substitute user/pass for relative stream urls on generic camera (#74201) Co-authored-by: Dave T --- homeassistant/components/generic/camera.py | 8 +++- .../components/generic/config_flow.py | 13 +++++- homeassistant/components/generic/strings.json | 4 ++ .../components/generic/translations/en.json | 11 ++--- tests/components/generic/test_config_flow.py | 46 +++++++++++++++++-- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 8b03f0a8ed3..961d3cecfb7 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -228,7 +228,13 @@ class GenericCamera(Camera): try: stream_url = self._stream_source.async_render(parse_result=False) url = yarl.URL(stream_url) - if not url.user and not url.password and self._username and self._password: + if ( + not url.user + and not url.password + and self._username + and self._password + and url.is_absolute() + ): url = url.with_user(self._username).with_password(self._password) return str(url) except TemplateError as err: diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 9096f2ce87e..514264f919e 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -150,6 +150,12 @@ async def async_test_still( except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", url, err) return {CONF_STILL_IMAGE_URL: "template_error"}, None + try: + yarl_url = yarl.URL(url) + except ValueError: + return {CONF_STILL_IMAGE_URL: "malformed_url"}, None + if not yarl_url.is_absolute(): + return {CONF_STILL_IMAGE_URL: "relative_url"}, None verify_ssl = info[CONF_VERIFY_SSL] auth = generate_auth(info) try: @@ -222,7 +228,12 @@ async def async_test_stream( if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True - url = yarl.URL(stream_source) + try: + url = yarl.URL(stream_source) + except ValueError: + return {CONF_STREAM_SOURCE: "malformed_url"} + if not url.is_absolute(): + return {CONF_STREAM_SOURCE: "relative_url"} if not url.user and not url.password: username = info.get(CONF_USERNAME) password = info.get(CONF_PASSWORD) diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 7d3cab19aa5..608c85c1379 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -6,6 +6,8 @@ "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", + "relative_url": "Relative URLs are not allowed", "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", @@ -75,6 +77,8 @@ "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", + "malformed_url": "[%key:component::generic::config::error::malformed_url%]", + "relative_url": "[%key:component::generic::config::error::relative_url%]", "template_error": "[%key:component::generic::config::error::template_error%]", "timeout": "[%key:component::generic::config::error::timeout%]", "stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index d01e6e59a4b..cb2200f9755 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,20 +1,19 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "relative_url": "Relative URLs are not allowed", "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -50,14 +49,12 @@ "error": { "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", - "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", - "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "relative_url": "Relative URLs are not allowed", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index f0589301014..592d139f92e 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -180,33 +180,43 @@ async def test_form_only_still_sample(hass, user_flow, image_file): @respx.mock @pytest.mark.parametrize( - ("template", "url", "expected_result"), + ("template", "url", "expected_result", "expected_errors"), [ # Test we can handle templates in strange parts of the url, #70961. ( "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", "http://localhost:8123/static/icons/favicon-apple-180x180.png", data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + None, ), ( "{% if 1 %}https://bla{% else %}https://yo{% endif %}", "https://bla/", data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + None, ), ( "http://{{example.org", "http://example.org", data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "template_error"}, ), ( "invalid1://invalid:4\\1", "invalid1://invalid:4%5c1", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "malformed_url"}, + ), + ( + "relative/urls/are/not/allowed.jpg", + "relative/urls/are/not/allowed.jpg", + data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "relative_url"}, ), ], ) async def test_still_template( - hass, user_flow, fakeimgbytes_png, template, url, expected_result + hass, user_flow, fakeimgbytes_png, template, url, expected_result, expected_errors ) -> None: """Test we can handle various templates.""" respx.get(url).respond(stream=fakeimgbytes_png) @@ -220,6 +230,7 @@ async def test_still_template( ) await hass.async_block_till_done() assert result2["type"] == expected_result + assert result2.get("errors") == expected_errors @respx.mock @@ -514,8 +525,29 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result4["flow_id"], user_input=data, ) - assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM - assert result5["errors"] == {"stream_source": "template_error"} + + assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5["errors"] == {"stream_source": "template_error"} + + # verify that an relative stream url is rejected. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1" + data[CONF_STREAM_SOURCE] = "relative/stream.mjpeg" + result6 = await hass.config_entries.options.async_configure( + result5["flow_id"], + user_input=data, + ) + assert result6.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result6["errors"] == {"stream_source": "relative_url"} + + # verify that an malformed stream url is rejected. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1" + data[CONF_STREAM_SOURCE] = "http://example.com:45:56" + result7 = await hass.config_entries.options.async_configure( + result6["flow_id"], + user_input=data, + ) + assert result7.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result7["errors"] == {"stream_source": "malformed_url"} async def test_slug(hass, caplog): @@ -528,6 +560,10 @@ async def test_slug(hass, caplog): assert result is None assert "Syntax error in" in caplog.text + result = slug(hass, "http://example.com:999999999999/stream") + assert result is None + assert "Syntax error in" in caplog.text + @respx.mock async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): From 30a5df58952ad45610ec322128af3fd0bdd3945c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jul 2022 15:47:54 -0500 Subject: [PATCH 087/820] Append name char value from the service to HomeKit Controller Entities (#74359) --- .../components/homekit_controller/__init__.py | 25 ++- .../components/homekit_controller/button.py | 2 +- .../components/homekit_controller/number.py | 8 +- .../components/homekit_controller/sensor.py | 60 +++-- .../components/homekit_controller/switch.py | 8 +- .../components/homekit_controller/utils.py | 5 + .../homekit_controller/fixtures/mss425f.json | 212 ++++++++++++++++++ .../homekit_controller/fixtures/mss565.json | 132 +++++++++++ .../specific_devices/test_aqara_gateway.py | 12 +- .../specific_devices/test_aqara_switch.py | 4 +- .../specific_devices/test_arlo_baby.py | 8 +- .../specific_devices/test_connectsense.py | 8 +- .../specific_devices/test_koogeek_ls1.py | 10 +- .../specific_devices/test_koogeek_p1eu.py | 4 +- .../specific_devices/test_koogeek_sw2.py | 10 +- .../specific_devices/test_mss425f.py | 73 ++++++ .../specific_devices/test_mss565.py | 42 ++++ .../specific_devices/test_mysa_living.py | 8 +- .../test_ryse_smart_bridge.py | 48 ++-- .../test_simpleconnect_fan.py | 4 +- .../specific_devices/test_velux_gateway.py | 16 +- .../test_vocolinc_flowerbud.py | 4 +- .../specific_devices/test_vocolinc_vp3.py | 4 +- .../homekit_controller/test_diagnostics.py | 92 ++++---- .../homekit_controller/test_init.py | 6 +- .../homekit_controller/test_light.py | 2 +- 26 files changed, 662 insertions(+), 145 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/mss425f.json create mode 100644 tests/components/homekit_controller/fixtures/mss565.json create mode 100644 tests/components/homekit_controller/specific_devices/test_mss425f.py create mode 100644 tests/components/homekit_controller/specific_devices/test_mss565.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 6909b226556..e5853c8ba66 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -27,7 +27,7 @@ from .config_flow import normalize_hkid from .connection import HKDevice, valid_serial_number from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .storage import EntityMapStorage -from .utils import async_get_controller +from .utils import async_get_controller, folded_name _LOGGER = logging.getLogger(__name__) @@ -42,6 +42,7 @@ class HomeKitEntity(Entity): self._accessory = accessory self._aid = devinfo["aid"] self._iid = devinfo["iid"] + self._char_name: str | None = None self._features = 0 self.setup() @@ -127,6 +128,9 @@ class HomeKitEntity(Entity): if CharacteristicPermissions.events in char.perms: self.watchable_characteristics.append((self._aid, char.iid)) + if self._char_name is None: + self._char_name = char.service.value(CharacteristicsTypes.NAME) + @property def unique_id(self) -> str: """Return the ID of this device.""" @@ -137,10 +141,27 @@ class HomeKitEntity(Entity): # Some accessories do not have a serial number return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" + @property + def default_name(self) -> str | None: + """Return the default name of the device.""" + return None + @property def name(self) -> str | None: """Return the name of the device if any.""" - return self.accessory.name + accessory_name = self.accessory.name + # If the service has a name char, use that, if not + # fallback to the default name provided by the subclass + device_name = self._char_name or self.default_name + folded_device_name = folded_name(device_name or "") + folded_accessory_name = folded_name(accessory_name) + if ( + device_name + and folded_accessory_name not in folded_device_name + and folded_device_name not in folded_accessory_name + ): + return f"{accessory_name} {device_name}" + return accessory_name @property def available(self) -> bool: diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index 7d2c737b509..e9c85dbe876 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -106,7 +106,7 @@ class HomeKitButton(CharacteristicEntity, ButtonEntity): @property def name(self) -> str: """Return the name of the device if any.""" - if name := super().name: + if name := self.accessory.name: return f"{name} {self.entity_description.name}" return f"{self.entity_description.name}" diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 4e0f5cfa077..07d22c27314 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -93,11 +93,11 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): super().__init__(conn, info, char) @property - def name(self) -> str | None: + def name(self) -> str: """Return the name of the device if any.""" - if prefix := super().name: - return f"{prefix} {self.entity_description.name}" - return self.entity_description.name + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index b7d7b8005ed..a3a722fea67 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -32,6 +32,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity from .connection import HKDevice +from .utils import folded_name CO2_ICON = "mdi:molecule-co2" @@ -199,7 +200,24 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { } -class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): +class HomeKitSensor(HomeKitEntity): + """Representation of a HomeKit sensor.""" + + @property + def name(self) -> str | None: + """Return the name of the device.""" + full_name = super().name + default_name = self.default_name + if ( + default_name + and full_name + and folded_name(default_name) not in folded_name(full_name) + ): + return f"{full_name} {default_name}" + return full_name + + +class HomeKitHumiditySensor(HomeKitSensor, SensorEntity): """Representation of a Homekit humidity sensor.""" _attr_device_class = SensorDeviceClass.HUMIDITY @@ -210,9 +228,9 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Humidity" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Humidity" @property def native_value(self) -> float: @@ -220,7 +238,7 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) -class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): +class HomeKitTemperatureSensor(HomeKitSensor, SensorEntity): """Representation of a Homekit temperature sensor.""" _attr_device_class = SensorDeviceClass.TEMPERATURE @@ -231,9 +249,9 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.TEMPERATURE_CURRENT] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Temperature" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Temperature" @property def native_value(self) -> float: @@ -241,7 +259,7 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) -class HomeKitLightSensor(HomeKitEntity, SensorEntity): +class HomeKitLightSensor(HomeKitSensor, SensorEntity): """Representation of a Homekit light level sensor.""" _attr_device_class = SensorDeviceClass.ILLUMINANCE @@ -252,9 +270,9 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Light Level" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Light Level" @property def native_value(self) -> int: @@ -273,9 +291,9 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} CO2" + def default_name(self) -> str: + """Return the default name of the device.""" + return "CO2" @property def native_value(self) -> int: @@ -283,7 +301,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL) -class HomeKitBatterySensor(HomeKitEntity, SensorEntity): +class HomeKitBatterySensor(HomeKitSensor, SensorEntity): """Representation of a Homekit battery sensor.""" _attr_device_class = SensorDeviceClass.BATTERY @@ -298,9 +316,9 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): ] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Battery" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Battery" @property def icon(self) -> str: @@ -374,7 +392,9 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): @property def name(self) -> str: """Return the name of the device if any.""" - return f"{super().name} {self.entity_description.name}" + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" @property def native_value(self) -> str | int | float: diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 07d0e21e59f..53b3958ecf6 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -147,11 +147,11 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity): super().__init__(conn, info, char) @property - def name(self) -> str | None: + def name(self) -> str: """Return the name of the device if any.""" - if prefix := super().name: - return f"{prefix} {self.entity_description.name}" - return self.entity_description.name + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 6831c3cee4a..892040d535f 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -10,6 +10,11 @@ from homeassistant.core import Event, HomeAssistant from .const import CONTROLLER +def folded_name(name: str) -> str: + """Return a name that is used for matching a similar string.""" + return name.casefold().replace(" ", "") + + async def async_get_controller(hass: HomeAssistant) -> Controller: """Get or create an aiohomekit Controller instance.""" if existing := hass.data.get(CONTROLLER): diff --git a/tests/components/homekit_controller/fixtures/mss425f.json b/tests/components/homekit_controller/fixtures/mss425f.json new file mode 100644 index 00000000000..35766b32d22 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/mss425f.json @@ -0,0 +1,212 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "Meross", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "MSS425F", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "MSS425F-15cc", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "HH41234", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "4.2.3", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.0.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 9, + "perms": ["pr"], + "format": "string", + "value": "2.0.1;16A75", + "maxLen": 64 + } + ] + }, + { + "iid": 10, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 12, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr"], + "format": "string", + "value": "Outlet-1", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 14, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 15, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": ["pr"], + "format": "string", + "value": "Outlet-2", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 18, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr"], + "format": "string", + "value": "Outlet-3", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 21, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 22, + "perms": ["pr"], + "format": "string", + "value": "Outlet-4", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 23, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 24, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 25, + "perms": ["pr"], + "format": "string", + "value": "USB", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 26, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/mss565.json b/tests/components/homekit_controller/fixtures/mss565.json new file mode 100644 index 00000000000..9ecb735dce3 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/mss565.json @@ -0,0 +1,132 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "Meross", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "MSS565", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "MSS565-28da", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "BB1121", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "4.1.9", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.0.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 9, + "perms": ["pr"], + "format": "string", + "value": "2.0.1;16A75", + "maxLen": 64 + } + ] + }, + { + "iid": 10, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 12, + "type": "00000043-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "iid": 14, + "perms": ["pr", "pw", "ev"], + "format": "int", + "value": 67, + "description": "Brightness", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 15, + "perms": ["pr"], + "format": "string", + "value": "Dimmer Switch", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index b2428cdc42b..6950f4cb61e 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -39,8 +39,8 @@ async def test_aqara_gateway_setup(hass): devices=[], entities=[ EntityTestInfo( - "alarm_control_panel.aqara_hub_1563", - friendly_name="Aqara Hub-1563", + "alarm_control_panel.aqara_hub_1563_security_system", + friendly_name="Aqara Hub-1563 Security System", unique_id="homekit-0000000123456789-66304", supported_features=SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_HOME @@ -48,8 +48,8 @@ async def test_aqara_gateway_setup(hass): state="disarmed", ), EntityTestInfo( - "light.aqara_hub_1563", - friendly_name="Aqara Hub-1563", + "light.aqara_hub_1563_lightbulb_1563", + friendly_name="Aqara Hub-1563 Lightbulb-1563", unique_id="homekit-0000000123456789-65792", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, @@ -98,8 +98,8 @@ async def test_aqara_gateway_e1_setup(hass): devices=[], entities=[ EntityTestInfo( - "alarm_control_panel.aqara_hub_e1_00a0", - friendly_name="Aqara-Hub-E1-00A0", + "alarm_control_panel.aqara_hub_e1_00a0_security_system", + friendly_name="Aqara-Hub-E1-00A0 Security System", unique_id="homekit-00aa00000a0-16", supported_features=SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_HOME diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py index e6dce42a1f7..16bef749429 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -38,8 +38,8 @@ async def test_aqara_switch_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="sensor.programmable_switch_battery", - friendly_name="Programmable Switch Battery", + entity_id="sensor.programmable_switch_battery_sensor", + friendly_name="Programmable Switch Battery Sensor", unique_id="homekit-111a1111a1a111-5", unit_of_measurement=PERCENTAGE, state="100", diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py index 9afe152c7b3..2cb312fc7f5 100644 --- a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -37,9 +37,9 @@ async def test_arlo_baby_setup(hass): state="idle", ), EntityTestInfo( - entity_id="binary_sensor.arlobabya0", + entity_id="binary_sensor.arlobabya0_motion", unique_id="homekit-00A0000000000-500", - friendly_name="ArloBabyA0", + friendly_name="ArloBabyA0 Motion", state="off", ), EntityTestInfo( @@ -71,9 +71,9 @@ async def test_arlo_baby_setup(hass): state="1", ), EntityTestInfo( - entity_id="light.arlobabya0", + entity_id="light.arlobabya0_nightlight", unique_id="homekit-00A0000000000-1100", - friendly_name="ArloBabyA0", + friendly_name="ArloBabyA0 Nightlight", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, state="off", diff --git a/tests/components/homekit_controller/specific_devices/test_connectsense.py b/tests/components/homekit_controller/specific_devices/test_connectsense.py index 2cbdf924319..fbb95fc3d89 100644 --- a/tests/components/homekit_controller/specific_devices/test_connectsense.py +++ b/tests/components/homekit_controller/specific_devices/test_connectsense.py @@ -59,8 +59,8 @@ async def test_connectsense_setup(hass): state="379.69299", ), EntityTestInfo( - entity_id="switch.inwall_outlet_0394de", - friendly_name="InWall Outlet-0394DE", + entity_id="switch.inwall_outlet_0394de_outlet_a", + friendly_name="InWall Outlet-0394DE Outlet A", unique_id="homekit-1020301376-13", state="on", ), @@ -89,8 +89,8 @@ async def test_connectsense_setup(hass): state="175.85001", ), EntityTestInfo( - entity_id="switch.inwall_outlet_0394de_2", - friendly_name="InWall Outlet-0394DE", + entity_id="switch.inwall_outlet_0394de_outlet_b", + friendly_name="InWall Outlet-0394DE Outlet B", unique_id="homekit-1020301376-25", state="on", ), diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index 88d483bd5bc..74525af1daf 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -43,8 +43,8 @@ async def test_koogeek_ls1_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="light.koogeek_ls1_20833f", - friendly_name="Koogeek-LS1-20833F", + entity_id="light.koogeek_ls1_20833f_light_strip", + friendly_name="Koogeek-LS1-20833F Light Strip", unique_id="homekit-AAAA011111111111-7", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, @@ -75,7 +75,11 @@ async def test_recover_from_failure(hass, utcnow, failure_cls): pairing.testing.events_enabled = False helper = Helper( - hass, "light.koogeek_ls1_20833f", pairing, accessories[0], config_entry + hass, + "light.koogeek_ls1_20833f_light_strip", + pairing, + accessories[0], + config_entry, ) # Set light state on fake device to off diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index f93adc732ba..bf8c86b7a7d 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -31,8 +31,8 @@ async def test_koogeek_p1eu_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.koogeek_p1_a00aa0", - friendly_name="Koogeek-P1-A00AA0", + entity_id="switch.koogeek_p1_a00aa0_outlet", + friendly_name="Koogeek-P1-A00AA0 outlet", unique_id="homekit-EUCP03190xxxxx48-7", state="off", ), diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index ed940cb6376..8307dc72f22 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -37,11 +37,17 @@ async def test_koogeek_sw2_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.koogeek_sw2_187a91", - friendly_name="Koogeek-SW2-187A91", + entity_id="switch.koogeek_sw2_187a91_switch_1", + friendly_name="Koogeek-SW2-187A91 Switch 1", unique_id="homekit-CNNT061751001372-8", state="off", ), + EntityTestInfo( + entity_id="switch.koogeek_sw2_187a91_switch_2", + friendly_name="Koogeek-SW2-187A91 Switch 2", + unique_id="homekit-CNNT061751001372-11", + state="off", + ), EntityTestInfo( entity_id="sensor.koogeek_sw2_187a91_power", friendly_name="Koogeek-SW2-187A91 Power", diff --git a/tests/components/homekit_controller/specific_devices/test_mss425f.py b/tests/components/homekit_controller/specific_devices/test_mss425f.py new file mode 100644 index 00000000000..3fe0ee739e6 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_mss425f.py @@ -0,0 +1,73 @@ +"""Tests for the Meross MSS425f power strip.""" + + +from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.helpers.entity import EntityCategory + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_meross_mss425f_setup(hass): + """Test that a MSS425f can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "mss425f.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="MSS425F-15cc", + model="MSS425F", + manufacturer="Meross", + sw_version="4.2.3", + hw_version="4.0.0", + serial_number="HH41234", + devices=[], + entities=[ + EntityTestInfo( + entity_id="button.mss425f_15cc_identify", + friendly_name="MSS425F-15cc Identify", + unique_id="homekit-HH41234-aid:1-sid:1-cid:2", + entity_category=EntityCategory.DIAGNOSTIC, + state=STATE_UNKNOWN, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_1", + friendly_name="MSS425F-15cc Outlet-1", + unique_id="homekit-HH41234-12", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_2", + friendly_name="MSS425F-15cc Outlet-2", + unique_id="homekit-HH41234-15", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_3", + friendly_name="MSS425F-15cc Outlet-3", + unique_id="homekit-HH41234-18", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_4", + friendly_name="MSS425F-15cc Outlet-4", + unique_id="homekit-HH41234-21", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_usb", + friendly_name="MSS425F-15cc USB", + unique_id="homekit-HH41234-24", + state=STATE_ON, + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_mss565.py b/tests/components/homekit_controller/specific_devices/test_mss565.py new file mode 100644 index 00000000000..0045a5ec507 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_mss565.py @@ -0,0 +1,42 @@ +"""Tests for the Meross MSS565 wall switch.""" + + +from homeassistant.const import STATE_ON + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_meross_mss565_setup(hass): + """Test that a MSS565 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "mss565.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="MSS565-28da", + model="MSS565", + manufacturer="Meross", + sw_version="4.1.9", + hw_version="4.0.0", + serial_number="BB1121", + devices=[], + entities=[ + EntityTestInfo( + entity_id="light.mss565_28da_dimmer_switch", + friendly_name="MSS565-28da Dimmer Switch", + unique_id="homekit-BB1121-12", + capabilities={"supported_color_modes": ["brightness"]}, + state=STATE_ON, + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_mysa_living.py b/tests/components/homekit_controller/specific_devices/test_mysa_living.py index 5829bd4e165..1a3bcdb2271 100644 --- a/tests/components/homekit_controller/specific_devices/test_mysa_living.py +++ b/tests/components/homekit_controller/specific_devices/test_mysa_living.py @@ -32,8 +32,8 @@ async def test_mysa_living_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="climate.mysa_85dda9", - friendly_name="Mysa-85dda9", + entity_id="climate.mysa_85dda9_thermostat", + friendly_name="Mysa-85dda9 Thermostat", unique_id="homekit-AAAAAAA000-20", supported_features=SUPPORT_TARGET_TEMPERATURE, capabilities={ @@ -60,8 +60,8 @@ async def test_mysa_living_setup(hass): state="24.1", ), EntityTestInfo( - entity_id="light.mysa_85dda9", - friendly_name="Mysa-85dda9", + entity_id="light.mysa_85dda9_display", + friendly_name="Mysa-85dda9 Display", unique_id="homekit-AAAAAAA000-40", supported_features=0, capabilities={"supported_color_modes": ["brightness"]}, diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index 51ebbfdc345..eb9c36d0b79 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -45,15 +45,15 @@ async def test_ryse_smart_bridge_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.master_bath_south", - friendly_name="Master Bath South", + entity_id="cover.master_bath_south_ryse_shade", + friendly_name="Master Bath South RYSE Shade", unique_id="homekit-00:00:00:00:00:00-2-48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), EntityTestInfo( - entity_id="sensor.master_bath_south_battery", - friendly_name="Master Bath South Battery", + entity_id="sensor.master_bath_south_ryse_shade_battery", + friendly_name="Master Bath South RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="100", @@ -71,15 +71,15 @@ async def test_ryse_smart_bridge_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.ryse_smartshade", - friendly_name="RYSE SmartShade", + entity_id="cover.ryse_smartshade_ryse_shade", + friendly_name="RYSE SmartShade RYSE Shade", unique_id="homekit-00:00:00:00:00:00-3-48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), EntityTestInfo( - entity_id="sensor.ryse_smartshade_battery", - friendly_name="RYSE SmartShade Battery", + entity_id="sensor.ryse_smartshade_ryse_shade_battery", + friendly_name="RYSE SmartShade RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -120,15 +120,15 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.lr_left", - friendly_name="LR Left", + entity_id="cover.lr_left_ryse_shade", + friendly_name="LR Left RYSE Shade", unique_id="homekit-00:00:00:00:00:00-2-48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), EntityTestInfo( - entity_id="sensor.lr_left_battery", - friendly_name="LR Left Battery", + entity_id="sensor.lr_left_ryse_shade_battery", + friendly_name="LR Left RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="89", @@ -146,15 +146,15 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.lr_right", - friendly_name="LR Right", + entity_id="cover.lr_right_ryse_shade", + friendly_name="LR Right RYSE Shade", unique_id="homekit-00:00:00:00:00:00-3-48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), EntityTestInfo( - entity_id="sensor.lr_right_battery", - friendly_name="LR Right Battery", + entity_id="sensor.lr_right_ryse_shade_battery", + friendly_name="LR Right RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -172,15 +172,15 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.br_left", - friendly_name="BR Left", + entity_id="cover.br_left_ryse_shade", + friendly_name="BR Left RYSE Shade", unique_id="homekit-00:00:00:00:00:00-4-48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), EntityTestInfo( - entity_id="sensor.br_left_battery", - friendly_name="BR Left Battery", + entity_id="sensor.br_left_ryse_shade_battery", + friendly_name="BR Left RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-4-64", unit_of_measurement=PERCENTAGE, state="100", @@ -198,15 +198,15 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.rzss", - friendly_name="RZSS", + entity_id="cover.rzss_ryse_shade", + friendly_name="RZSS RYSE Shade", unique_id="homekit-00:00:00:00:00:00-5-48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), EntityTestInfo( - entity_id="sensor.rzss_battery", - friendly_name="RZSS Battery", + entity_id="sensor.rzss_ryse_shade_battery", + friendly_name="RZSS RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-5-64", unit_of_measurement=PERCENTAGE, state="0", diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py index d3531e1c65f..f160169a43c 100644 --- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -34,8 +34,8 @@ async def test_simpleconnect_fan_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="fan.simpleconnect_fan_06f674", - friendly_name="SIMPLEconnect Fan-06F674", + entity_id="fan.simpleconnect_fan_06f674_hunter_fan", + friendly_name="SIMPLEconnect Fan-06F674 Hunter Fan", unique_id="homekit-1234567890abcd-8", supported_features=SUPPORT_DIRECTION | SUPPORT_SET_SPEED, capabilities={ diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py index d8d73709c49..21aa91fc933 100644 --- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -52,8 +52,8 @@ async def test_velux_cover_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.velux_window", - friendly_name="VELUX Window", + entity_id="cover.velux_window_roof_window", + friendly_name="VELUX Window Roof Window", unique_id="homekit-1111111a114a111a-8", supported_features=SUPPORT_CLOSE | SUPPORT_SET_POSITION @@ -73,22 +73,22 @@ async def test_velux_cover_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="sensor.velux_sensor_temperature", - friendly_name="VELUX Sensor Temperature", + entity_id="sensor.velux_sensor_temperature_sensor", + friendly_name="VELUX Sensor Temperature sensor", unique_id="homekit-a11b111-8", unit_of_measurement=TEMP_CELSIUS, state="18.9", ), EntityTestInfo( - entity_id="sensor.velux_sensor_humidity", - friendly_name="VELUX Sensor Humidity", + entity_id="sensor.velux_sensor_humidity_sensor", + friendly_name="VELUX Sensor Humidity sensor", unique_id="homekit-a11b111-11", unit_of_measurement=PERCENTAGE, state="58", ), EntityTestInfo( - entity_id="sensor.velux_sensor_co2", - friendly_name="VELUX Sensor CO2", + entity_id="sensor.velux_sensor_carbon_dioxide_sensor", + friendly_name="VELUX Sensor Carbon Dioxide sensor", unique_id="homekit-a11b111-14", unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state="400", diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index 6fa9ff63690..f788b016ba2 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -46,8 +46,8 @@ async def test_vocolinc_flowerbud_setup(hass): state="off", ), EntityTestInfo( - entity_id="light.vocolinc_flowerbud_0d324b", - friendly_name="VOCOlinc-Flowerbud-0d324b", + entity_id="light.vocolinc_flowerbud_0d324b_mood_light", + friendly_name="VOCOlinc-Flowerbud-0d324b Mood Light", unique_id="homekit-AM01121849000327-9", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py index da69b7fe309..3a3579b8781 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py @@ -31,8 +31,8 @@ async def test_vocolinc_vp3_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.vocolinc_vp3_123456", - friendly_name="VOCOlinc-VP3-123456", + entity_id="switch.vocolinc_vp3_123456_outlet", + friendly_name="VOCOlinc-VP3-123456 Outlet", unique_id="homekit-EU0121203xxxxx07-48", state="on", ), diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index 72ef571e214..e770279752e 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -234,28 +234,6 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "sw_version": "2.2.15", "hw_version": "", "entities": [ - { - "original_name": "Koogeek-LS1-20833F", - "disabled": False, - "disabled_by": None, - "entity_category": None, - "device_class": None, - "original_device_class": None, - "icon": None, - "original_icon": None, - "unit_of_measurement": None, - "state": { - "entity_id": "light.koogeek_ls1_20833f", - "state": "off", - "attributes": { - "supported_color_modes": ["hs"], - "friendly_name": "Koogeek-LS1-20833F", - "supported_features": 0, - }, - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", - }, - }, { "device_class": None, "disabled": False, @@ -276,6 +254,28 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc }, "unit_of_measurement": None, }, + { + "device_class": None, + "disabled": False, + "disabled_by": None, + "entity_category": None, + "icon": None, + "original_device_class": None, + "original_icon": None, + "original_name": "Koogeek-LS1-20833F Light Strip", + "state": { + "attributes": { + "friendly_name": "Koogeek-LS1-20833F Light Strip", + "supported_color_modes": ["hs"], + "supported_features": 0, + }, + "entity_id": "light.koogeek_ls1_20833f_light_strip", + "last_changed": "2023-01-01T00:00:00+00:00", + "last_updated": "2023-01-01T00:00:00+00:00", + "state": "off", + }, + "unit_of_measurement": None, + }, ], } ], @@ -504,28 +504,6 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "sw_version": "2.2.15", "hw_version": "", "entities": [ - { - "original_name": "Koogeek-LS1-20833F", - "disabled": False, - "disabled_by": None, - "entity_category": None, - "device_class": None, - "original_device_class": None, - "icon": None, - "original_icon": None, - "unit_of_measurement": None, - "state": { - "entity_id": "light.koogeek_ls1_20833f", - "state": "off", - "attributes": { - "supported_color_modes": ["hs"], - "friendly_name": "Koogeek-LS1-20833F", - "supported_features": 0, - }, - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", - }, - }, { "device_class": None, "disabled": False, @@ -536,7 +514,9 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "original_icon": None, "original_name": "Koogeek-LS1-20833F Identify", "state": { - "attributes": {"friendly_name": "Koogeek-LS1-20833F Identify"}, + "attributes": { + "friendly_name": "Koogeek-LS1-20833F " "Identify" + }, "entity_id": "button.koogeek_ls1_20833f_identify", "last_changed": "2023-01-01T00:00:00+00:00", "last_updated": "2023-01-01T00:00:00+00:00", @@ -544,6 +524,28 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): }, "unit_of_measurement": None, }, + { + "device_class": None, + "disabled": False, + "disabled_by": None, + "entity_category": None, + "icon": None, + "original_device_class": None, + "original_icon": None, + "original_name": "Koogeek-LS1-20833F Light Strip", + "state": { + "attributes": { + "friendly_name": "Koogeek-LS1-20833F Light Strip", + "supported_color_modes": ["hs"], + "supported_features": 0, + }, + "entity_id": "light.koogeek_ls1_20833f_light_strip", + "last_changed": "2023-01-01T00:00:00+00:00", + "last_updated": "2023-01-01T00:00:00+00:00", + "state": "off", + }, + "unit_of_measurement": None, + }, ], }, } diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 820b89e587d..84a6c8b86bf 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -16,8 +16,8 @@ from .common import Helper, remove_device from tests.components.homekit_controller.common import setup_test_component -ALIVE_DEVICE_NAME = "Light Bulb" -ALIVE_DEEVICE_ENTITY_ID = "light.testdevice" +ALIVE_DEVICE_NAME = "testdevice" +ALIVE_DEVICE_ENTITY_ID = "light.testdevice" def create_motion_sensor_service(accessory): @@ -72,7 +72,7 @@ async def test_device_remove_devices(hass, hass_ws_client): entry_id = config_entry.entry_id registry: EntityRegistry = er.async_get(hass) - entity = registry.entities[ALIVE_DEEVICE_ENTITY_ID] + entity = registry.entities[ALIVE_DEVICE_ENTITY_ID] device_registry = dr.async_get(hass) live_device_entry = device_registry.async_get(entity.device_id) diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index 39e44c57cc5..83bddf3d26a 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -12,7 +12,7 @@ from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_UNAVAILABLE from tests.components.homekit_controller.common import setup_test_component -LIGHT_BULB_NAME = "Light Bulb" +LIGHT_BULB_NAME = "TestDevice" LIGHT_BULB_ENTITY_ID = "light.testdevice" From 40ed44cbea2018270c1a2a97b2b3a32d445701aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jul 2022 15:48:34 -0500 Subject: [PATCH 088/820] Fix esphome state mapping (#74337) --- homeassistant/components/esphome/__init__.py | 17 +++--- .../components/esphome/entry_data.py | 53 ++++--------------- 2 files changed, 22 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0c1eac3aa45..ddedaf11ceb 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -558,7 +558,7 @@ async def platform_async_setup_entry( entry_data: RuntimeEntryData = DomainData.get(hass).get_entry_data(entry) entry_data.info[component_key] = {} entry_data.old_info[component_key] = {} - entry_data.state[component_key] = {} + entry_data.state.setdefault(state_type, {}) @callback def async_list_entities(infos: list[EntityInfo]) -> None: @@ -578,7 +578,7 @@ async def platform_async_setup_entry( old_infos.pop(info.key) else: # Create new entity - entity = entity_type(entry_data, component_key, info.key) + entity = entity_type(entry_data, component_key, info.key, state_type) add_entities.append(entity) new_infos[info.key] = info @@ -677,12 +677,17 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" def __init__( - self, entry_data: RuntimeEntryData, component_key: str, key: int + self, + entry_data: RuntimeEntryData, + component_key: str, + key: int, + state_type: type[_StateT], ) -> None: """Initialize.""" self._entry_data = entry_data self._component_key = component_key self._key = key + self._state_type = state_type async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -707,7 +712,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): self.async_on_remove( self._entry_data.async_subscribe_state_update( - self._component_key, self._key, self._on_state_update + self._state_type, self._key, self._on_state_update ) ) @@ -755,11 +760,11 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): @property def _state(self) -> _StateT: - return cast(_StateT, self._entry_data.state[self._component_key][self._key]) + return cast(_StateT, self._entry_data.state[self._state_type][self._key]) @property def _has_state(self) -> bool: - return self._key in self._entry_data.state[self._component_key] + return self._key in self._entry_data.state[self._state_type] @property def available(self) -> bool: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 8eb56e6fdb6..41a0e89245e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -12,34 +12,21 @@ from aioesphomeapi import ( APIClient, APIVersion, BinarySensorInfo, - BinarySensorState, CameraInfo, - CameraState, ClimateInfo, - ClimateState, CoverInfo, - CoverState, DeviceInfo, EntityInfo, EntityState, FanInfo, - FanState, LightInfo, - LightState, LockInfo, - LockState, MediaPlayerInfo, - MediaPlayerState, NumberInfo, - NumberState, SelectInfo, - SelectState, SensorInfo, - SensorState, SwitchInfo, - SwitchState, TextSensorInfo, - TextSensorState, UserService, ) from aioesphomeapi.model import ButtonInfo @@ -56,8 +43,8 @@ _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { BinarySensorInfo: Platform.BINARY_SENSOR, - ButtonInfo: Platform.BINARY_SENSOR, - CameraInfo: Platform.BINARY_SENSOR, + ButtonInfo: Platform.BUTTON, + CameraInfo: Platform.CAMERA, ClimateInfo: Platform.CLIMATE, CoverInfo: Platform.COVER, FanInfo: Platform.FAN, @@ -71,23 +58,6 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { TextSensorInfo: Platform.SENSOR, } -STATE_TYPE_TO_COMPONENT_KEY = { - BinarySensorState: Platform.BINARY_SENSOR, - EntityState: Platform.BINARY_SENSOR, - CameraState: Platform.BINARY_SENSOR, - ClimateState: Platform.CLIMATE, - CoverState: Platform.COVER, - FanState: Platform.FAN, - LightState: Platform.LIGHT, - LockState: Platform.LOCK, - MediaPlayerState: Platform.MEDIA_PLAYER, - NumberState: Platform.NUMBER, - SelectState: Platform.SELECT, - SensorState: Platform.SENSOR, - SwitchState: Platform.SWITCH, - TextSensorState: Platform.SENSOR, -} - @dataclass class RuntimeEntryData: @@ -96,7 +66,7 @@ class RuntimeEntryData: entry_id: str client: APIClient store: Store - state: dict[str, dict[int, EntityState]] = field(default_factory=dict) + state: dict[type[EntityState], dict[int, EntityState]] = field(default_factory=dict) info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) # A second list of EntityInfo objects @@ -111,9 +81,9 @@ class RuntimeEntryData: api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) - state_subscriptions: dict[tuple[str, int], Callable[[], None]] = field( - default_factory=dict - ) + state_subscriptions: dict[ + tuple[type[EntityState], int], Callable[[], None] + ] = field(default_factory=dict) loaded_platforms: set[str] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None @@ -160,24 +130,23 @@ class RuntimeEntryData: @callback def async_subscribe_state_update( self, - component_key: str, + state_type: type[EntityState], state_key: int, entity_callback: Callable[[], None], ) -> Callable[[], None]: """Subscribe to state updates.""" def _unsubscribe() -> None: - self.state_subscriptions.pop((component_key, state_key)) + self.state_subscriptions.pop((state_type, state_key)) - self.state_subscriptions[(component_key, state_key)] = entity_callback + self.state_subscriptions[(state_type, state_key)] = entity_callback return _unsubscribe @callback def async_update_state(self, state: EntityState) -> None: """Distribute an update of state information to the target.""" - component_key = STATE_TYPE_TO_COMPONENT_KEY[type(state)] - subscription_key = (component_key, state.key) - self.state[component_key][state.key] = state + subscription_key = (type(state), state.key) + self.state[type(state)][state.key] = state _LOGGER.debug( "Dispatching update with key %s: %s", subscription_key, From 5bd9c5aee8e748d0d27e73c80e71cf3b62cf07ff Mon Sep 17 00:00:00 2001 From: mbo18 Date: Sun, 3 Jul 2022 22:49:03 +0200 Subject: [PATCH 089/820] Migrate Meteo_france to native_* (#74297) --- .../components/meteo_france/weather.py | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index cca1f6fe684..a30a65304b0 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -4,16 +4,22 @@ import time from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODE, TEMP_CELSIUS +from homeassistant.const import ( + CONF_MODE, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -71,6 +77,11 @@ async def async_setup_entry( class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + def __init__(self, coordinator: DataUpdateCoordinator, mode: str) -> None: """Initialise the platform with a data instance and station name.""" super().__init__(coordinator) @@ -107,17 +118,12 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data.current_forecast["T"]["value"] @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data.current_forecast["sea_level"] @@ -127,10 +133,9 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.current_forecast["humidity"] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - # convert from API m/s to km/h - return round(self.coordinator.data.current_forecast["wind"]["speed"] * 3.6) + return self.coordinator.data.current_forecast["wind"]["speed"] @property def wind_bearing(self): @@ -158,9 +163,9 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ATTR_FORECAST_CONDITION: format_condition( forecast["weather"]["desc"] ), - ATTR_FORECAST_TEMP: forecast["T"]["value"], - ATTR_FORECAST_PRECIPITATION: forecast["rain"].get("1h"), - ATTR_FORECAST_WIND_SPEED: forecast["wind"]["speed"], + ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["value"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["rain"].get("1h"), + ATTR_FORECAST_NATIVE_WIND_SPEED: forecast["wind"]["speed"], ATTR_FORECAST_WIND_BEARING: forecast["wind"]["direction"] if forecast["wind"]["direction"] != -1 else None, @@ -179,9 +184,11 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ATTR_FORECAST_CONDITION: format_condition( forecast["weather12H"]["desc"] ), - ATTR_FORECAST_TEMP: forecast["T"]["max"], - ATTR_FORECAST_TEMP_LOW: forecast["T"]["min"], - ATTR_FORECAST_PRECIPITATION: forecast["precipitation"]["24h"], + ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"], + ATTR_FORECAST_NATIVE_TEMP_LOW: forecast["T"]["min"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["precipitation"][ + "24h" + ], } ) return forecast_data From d91f5b77c876de156fedd00cf02e109ee63ec3b9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 3 Jul 2022 22:53:44 +0200 Subject: [PATCH 090/820] Fix unique id issue for OpenWeatherMap (#74335) --- .../components/openweathermap/const.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index f180f2a9bbf..06f13daa9c2 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -22,10 +22,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_PRESSURE, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ) @@ -72,6 +68,11 @@ ATTR_API_FORECAST = "forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] +ATTR_FORECAST_PRECIPITATION = "precipitation" +ATTR_FORECAST_PRESSURE = "pressure" +ATTR_FORECAST_TEMP = "temperature" +ATTR_FORECAST_TEMP_LOW = "templow" + FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" FORECAST_MODE_FREE_DAILY = "freedaily" @@ -266,7 +267,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRECIPITATION, + key=ATTR_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=LENGTH_MILLIMETERS, ), @@ -276,19 +277,19 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRESSURE, + key=ATTR_FORECAST_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP, + key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP_LOW, + key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, From 02c7261b74edcf7b4a6f10dc998bb50983ca9360 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jul 2022 16:32:30 -0500 Subject: [PATCH 091/820] Small naming improvements and basic tests for ecobee 501s (#74370) --- .../components/homekit_controller/__init__.py | 16 +- .../fixtures/ecobee_501.json | 475 ++++++++++++++++++ .../specific_devices/test_ecobee_501.py | 65 +++ .../specific_devices/test_hue_bridge.py | 2 +- 4 files changed, 551 insertions(+), 7 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/ecobee_501.json create mode 100644 tests/components/homekit_controller/specific_devices/test_ecobee_501.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index e5853c8ba66..67da7a1068f 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -155,12 +155,16 @@ class HomeKitEntity(Entity): device_name = self._char_name or self.default_name folded_device_name = folded_name(device_name or "") folded_accessory_name = folded_name(accessory_name) - if ( - device_name - and folded_accessory_name not in folded_device_name - and folded_device_name not in folded_accessory_name - ): - return f"{accessory_name} {device_name}" + if device_name: + # Sometimes the device name includes the accessory + # name already like My ecobee Occupancy / My ecobee + if folded_device_name.startswith(folded_accessory_name): + return device_name + if ( + folded_accessory_name not in folded_device_name + and folded_device_name not in folded_accessory_name + ): + return f"{accessory_name} {device_name}" return accessory_name @property diff --git a/tests/components/homekit_controller/fixtures/ecobee_501.json b/tests/components/homekit_controller/fixtures/ecobee_501.json new file mode 100644 index 00000000000..245abe974de --- /dev/null +++ b/tests/components/homekit_controller/fixtures/ecobee_501.json @@ -0,0 +1,475 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "ECB501", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "My ecobee", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "123456789016", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.7.340214", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 11, + "perms": ["pr", "hd"], + "format": "string", + "value": "4.1;3fac0fb4", + "maxLen": 64 + }, + { + "type": "00000220-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "hd"], + "format": "data", + "value": "u4qz9c7m80Y=" + }, + { + "type": "000000A6-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": ["pr", "ev"], + "format": "uint32", + "value": 0, + "description": "Accessory Flags" + } + ] + }, + { + "iid": 30, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 31, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 16, + "type": "0000004A-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000000F-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Current Heating Cooling State", + "minValue": 0, + "maxValue": 2, + "minStep": 1, + "valid-values": [0, 1, 2] + }, + { + "type": "00000033-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 3, + "description": "Target Heating Cooling State", + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "valid-values": [0, 1, 2, 3] + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr", "ev"], + "format": "float", + "value": 21.3, + "description": "Current Temperature", + "unit": "celsius", + "minValue": 0, + "maxValue": 40.0, + "minStep": 0.1 + }, + { + "type": "00000035-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 25.6, + "description": "Target Temperature", + "unit": "celsius", + "minValue": 7.2, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "00000036-0000-1000-8000-0026BB765291", + "iid": 21, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Temperature Display Units", + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "valid-values": [0, 1] + }, + { + "type": "0000000D-0000-1000-8000-0026BB765291", + "iid": 22, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 25.6, + "description": "Cooling Threshold Temperature", + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "00000012-0000-1000-8000-0026BB765291", + "iid": 23, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 7.2, + "description": "Heating Threshold Temperature", + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 24, + "perms": ["pr", "ev"], + "format": "float", + "value": 55.0, + "description": "Current Relative Humidity", + "unit": "percentage", + "minValue": 0, + "maxValue": 100.0, + "minStep": 1.0 + }, + { + "type": "00000034-0000-1000-8000-0026BB765291", + "iid": 25, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 36.0, + "description": "Target Relative Humidity", + "unit": "percentage", + "minValue": 20.0, + "maxValue": 50.0, + "minStep": 1.0 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 27, + "perms": ["pr"], + "format": "string", + "value": "My ecobee", + "description": "Name", + "maxLen": 64 + }, + { + "type": "000000BF-0000-1000-8000-0026BB765291", + "iid": 75, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Target Fan State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "000000AF-0000-1000-8000-0026BB765291", + "iid": 76, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Current Fan State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "B7DDB9A3-54BB-4572-91D2-F1F5B0510F8C", + "iid": 33, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "E4489BBC-5227-4569-93E5-B345E3E5508F", + "iid": 34, + "perms": ["pr", "pw"], + "format": "float", + "value": 20.6, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "7D381BAA-20F9-40E5-9BE9-AEB92D4BECEF", + "iid": 35, + "perms": ["pr", "pw"], + "format": "float", + "value": 25.6, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "73AAB542-892A-4439-879A-D2A883724B69", + "iid": 36, + "perms": ["pr", "pw"], + "format": "float", + "value": 17.8, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "5DA985F0-898A-4850-B987-B76C6C78D670", + "iid": 37, + "perms": ["pr", "pw"], + "format": "float", + "value": 27.8, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "05B97374-6DC0-439B-A0FA-CA33F612D425", + "iid": 38, + "perms": ["pr", "pw"], + "format": "float", + "value": 18.3, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "A251F6E7-AC46-4190-9C5D-3D06277BDF9F", + "iid": 39, + "perms": ["pr", "pw"], + "format": "float", + "value": 27.8, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "1B300BC2-CFFC-47FF-89F9-BD6CCF5F2853", + "iid": 40, + "perms": ["pw"], + "format": "uint8", + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "1621F556-1367-443C-AF19-82AF018E99DE", + "iid": 41, + "perms": ["pr", "pw"], + "format": "string", + "value": "2014-01-03T00:00:00-04:00Q", + "maxLen": 64 + }, + { + "type": "FA128DE6-9D7D-49A4-B6D8-4E4E234DEE38", + "iid": 48, + "perms": ["pw"], + "format": "bool" + }, + { + "type": "4A6AE4F6-036C-495D-87CC-B3702B437741", + "iid": 49, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 4, + "minStep": 1 + }, + { + "type": "DB7BF261-7042-4194-8BD1-3AA22830AEDD", + "iid": 50, + "perms": ["pr"], + "format": "uint8", + "value": 2, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "41935E3E-B54D-42E9-B8B9-D33C6319F0AF", + "iid": 51, + "perms": ["pr"], + "format": "bool", + "value": false + }, + { + "type": "C35DA3C0-E004-40E3-B153-46655CDD9214", + "iid": 52, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 0, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "48F62AEC-4171-4B4A-8F0E-1EEB6708B3FB", + "iid": 53, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "1B1515F2-CC45-409F-991F-C480987F92C3", + "iid": 54, + "perms": ["pr"], + "format": "string", + "value": "The Hive is humming along. You have no pending alerts or reminders.", + "maxLen": 64 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "My ecobee Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Motion Detected" + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr", "ev"], + "format": "int", + "value": 14, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "My ecobee Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 1, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr", "ev"], + "format": "int", + "value": 44, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py new file mode 100644 index 00000000000..89443008683 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py @@ -0,0 +1,65 @@ +"""Tests for Ecobee 501.""" + + +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.const import STATE_ON + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_ecobee501_setup(hass): + """Test that a Ecobee 501 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "ecobee_501.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="My ecobee", + model="ECB501", + manufacturer="ecobee Inc.", + sw_version="4.7.340214", + hw_version="", + serial_number="123456789016", + devices=[], + entities=[ + EntityTestInfo( + entity_id="climate.my_ecobee", + friendly_name="My ecobee", + unique_id="homekit-123456789016-16", + supported_features=( + SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_TARGET_HUMIDITY + ), + capabilities={ + "hvac_modes": ["off", "heat", "cool", "heat_cool"], + "min_temp": 7.2, + "max_temp": 33.3, + "min_humidity": 20, + "max_humidity": 50, + }, + state="heat_cool", + ), + EntityTestInfo( + entity_id="binary_sensor.my_ecobee_occupancy", + friendly_name="My ecobee Occupancy", + unique_id="homekit-123456789016-57", + unit_of_measurement=None, + state=STATE_ON, + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 76d09629064..52a5fb83972 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -41,7 +41,7 @@ async def test_hue_bridge_setup(hass): entities=[ EntityTestInfo( entity_id="sensor.hue_dimmer_switch_battery", - friendly_name="Hue dimmer switch Battery", + friendly_name="Hue dimmer switch battery", unique_id="homekit-6623462389072572-644245094400", unit_of_measurement=PERCENTAGE, state="100", From 269e414e848853b7f7f3d50e6415b5708364fbae Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 4 Jul 2022 00:27:42 +0000 Subject: [PATCH 092/820] [ci skip] Translation update --- .../accuweather/translations/pl.json | 2 +- .../components/airly/translations/pl.json | 2 +- .../components/aurora/translations/sv.json | 11 ++++++ .../components/generic/translations/en.json | 7 ++++ .../components/generic/translations/fr.json | 4 +++ .../components/gios/translations/pl.json | 2 +- .../homeassistant/translations/de.json | 1 + .../homeassistant/translations/el.json | 1 + .../homeassistant/translations/ja.json | 1 + .../homeassistant/translations/pl.json | 1 + .../homeassistant/translations/zh-Hant.json | 1 + .../components/nextdns/translations/en.json | 24 +++++++++++++ .../components/nextdns/translations/fr.json | 24 +++++++++++++ .../components/nextdns/translations/pl.json | 24 +++++++++++++ .../components/threshold/translations/sv.json | 5 +++ .../utility_meter/translations/sv.json | 34 ++++++++++++++++++- 16 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/aurora/translations/sv.json create mode 100644 homeassistant/components/nextdns/translations/en.json create mode 100644 homeassistant/components/nextdns/translations/fr.json create mode 100644 homeassistant/components/nextdns/translations/pl.json diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index 764218f8e11..4ec4d05a932 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -34,7 +34,7 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera AccuWeather", + "can_reach_server": "Dost\u0119p do serwera", "remaining_requests": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" } } diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index 72d35ec97a7..9ae6243d2fb 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera Airly", + "can_reach_server": "Dost\u0119p do serwera", "requests_per_day": "Dozwolone dzienne \u017c\u0105dania", "requests_remaining": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" } diff --git a/homeassistant/components/aurora/translations/sv.json b/homeassistant/components/aurora/translations/sv.json new file mode 100644 index 00000000000..7e16a2c036e --- /dev/null +++ b/homeassistant/components/aurora/translations/sv.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "threshold": "Gr\u00e4nsv\u00e4rde (%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index cb2200f9755..a4e96718225 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -13,7 +14,9 @@ "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -52,9 +55,13 @@ "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "relative_url": "Relative URLs are not allowed", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json index 0d517a846e7..2c992c2fa4f 100644 --- a/homeassistant/components/generic/translations/fr.json +++ b/homeassistant/components/generic/translations/fr.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Une cam\u00e9ra avec ces param\u00e8tres d'URL existe d\u00e9j\u00e0.", "invalid_still_image": "L'URL n'a pas renvoy\u00e9 d'image fixe valide", + "malformed_url": "URL mal form\u00e9e", "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", + "relative_url": "Les URL relatives ne sont pas autoris\u00e9es", "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Une cam\u00e9ra avec ces param\u00e8tres d'URL existe d\u00e9j\u00e0.", "invalid_still_image": "L'URL n'a pas renvoy\u00e9 d'image fixe valide", + "malformed_url": "URL mal form\u00e9e", "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", + "relative_url": "Les URL relatives ne sont pas autoris\u00e9es", "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", diff --git a/homeassistant/components/gios/translations/pl.json b/homeassistant/components/gios/translations/pl.json index 6a7d2aa7064..475ab76b576 100644 --- a/homeassistant/components/gios/translations/pl.json +++ b/homeassistant/components/gios/translations/pl.json @@ -20,7 +20,7 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera GIO\u015a" + "can_reach_server": "Dost\u0119p do serwera" } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json index 54909cb3c24..756670e5a47 100644 --- a/homeassistant/components/homeassistant/translations/de.json +++ b/homeassistant/components/homeassistant/translations/de.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU-Architektur", + "config_dir": "Konfigurationsverzeichnis", "dev": "Entwicklung", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/el.json b/homeassistant/components/homeassistant/translations/el.json index 616ad96a867..9c345d511e1 100644 --- a/homeassistant/components/homeassistant/translations/el.json +++ b/homeassistant/components/homeassistant/translations/el.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "\u0391\u03c1\u03c7\u03b9\u03c4\u03b5\u03ba\u03c4\u03bf\u03bd\u03b9\u03ba\u03ae CPU", + "config_dir": "\u039a\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd", "dev": "\u0391\u03bd\u03ac\u03c0\u03c4\u03c5\u03be\u03b7", "docker": "Docker", "hassio": "\u0395\u03c0\u03cc\u03c0\u03c4\u03b7\u03c2", diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json index 14b1deb55c8..8be012c7c1a 100644 --- a/homeassistant/components/homeassistant/translations/ja.json +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU\u30a2\u30fc\u30ad\u30c6\u30af\u30c1\u30e3", + "config_dir": "\u30b3\u30f3\u30d5\u30a3\u30ae\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u30c7\u30a3\u30ec\u30af\u30c8\u30ea", "dev": "\u30c7\u30a3\u30d9\u30ed\u30c3\u30d7\u30e1\u30f3\u30c8", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/pl.json b/homeassistant/components/homeassistant/translations/pl.json index 9f85cc4ff15..bdf26a8b49d 100644 --- a/homeassistant/components/homeassistant/translations/pl.json +++ b/homeassistant/components/homeassistant/translations/pl.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Architektura procesora", + "config_dir": "Folder konfiguracji", "dev": "Wersja deweloperska", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json index 03812830b26..c42acf960c6 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hant.json +++ b/homeassistant/components/homeassistant/translations/zh-Hant.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU \u67b6\u69cb", + "config_dir": "\u8a2d\u5b9a\u76ee\u9304", "dev": "\u958b\u767c\u7248", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/nextdns/translations/en.json b/homeassistant/components/nextdns/translations/en.json new file mode 100644 index 00000000000..c765d53a4d9 --- /dev/null +++ b/homeassistant/components/nextdns/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "This NextDNS profile is already configured." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "unknown": "Unexpected error" + }, + "step": { + "profiles": { + "data": { + "profile": "Profile" + } + }, + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/fr.json b/homeassistant/components/nextdns/translations/fr.json new file mode 100644 index 00000000000..bfebc9078a5 --- /dev/null +++ b/homeassistant/components/nextdns/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ce profil NextDNS est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 d'API non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pl.json b/homeassistant/components/nextdns/translations/pl.json new file mode 100644 index 00000000000..b7b74af7f1b --- /dev/null +++ b/homeassistant/components/nextdns/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ten profil NextDNS jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "Klucz API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/sv.json b/homeassistant/components/threshold/translations/sv.json index d19b6cabb91..dd247031a4e 100644 --- a/homeassistant/components/threshold/translations/sv.json +++ b/homeassistant/components/threshold/translations/sv.json @@ -2,6 +2,11 @@ "config": { "error": { "need_lower_upper": "Undre och \u00f6vre gr\u00e4ns kan inte vara tomma" + }, + "step": { + "user": { + "title": "L\u00e4gg till gr\u00e4nsv\u00e4rdessensor" + } } }, "options": { diff --git a/homeassistant/components/utility_meter/translations/sv.json b/homeassistant/components/utility_meter/translations/sv.json index 38d433e7b42..6ef91919b73 100644 --- a/homeassistant/components/utility_meter/translations/sv.json +++ b/homeassistant/components/utility_meter/translations/sv.json @@ -1,3 +1,35 @@ { - "title": "Tj\u00e4nsten \u00e4r redan konfigurerad" + "config": { + "step": { + "user": { + "data": { + "cycle": "\u00c5terst\u00e4ll m\u00e4tarcykel", + "delta_values": "Delta-v\u00e4rden", + "name": "Namn", + "net_consumption": "Nettof\u00f6rbrukning", + "offset": "\u00c5terst\u00e4ll m\u00e4taroffset", + "source": "Sensorsk\u00e4lla", + "tariffs": "Tariffer som st\u00f6ds" + }, + "data_description": { + "delta_values": "Aktivera om k\u00e4llv\u00e4rdena \u00e4r deltav\u00e4rden sedan den senaste avl\u00e4sningen ist\u00e4llet f\u00f6r absoluta v\u00e4rden.", + "net_consumption": "Aktivera om k\u00e4llan \u00e4r en nettom\u00e4tare, vilket betyder att den b\u00e5de kan \u00f6ka och minska.", + "offset": "F\u00f6rskjut dagen f\u00f6r en m\u00e5natlig m\u00e4tar\u00e5terst\u00e4llning.", + "tariffs": "En lista \u00f6ver st\u00f6dda tariffer, l\u00e4mna tom om bara en enda tariff beh\u00f6vs." + }, + "description": "Skapa en sensor som \u00f6vervakar f\u00f6rbrukningen av t.ex. energi, gas, vatten, v\u00e4rme, \u00f6ver en konfigurerad tidsperiod, vanligtvis m\u00e5nadsvis. M\u00e4tarsensorn st\u00f6djer valfritt att dela upp f\u00f6rbrukningen efter tariffer, i s\u00e5 fall skapas en sensor f\u00f6r varje tariff samt en val entitet f\u00f6r att v\u00e4lja den aktuella tariffen.", + "title": "L\u00e4gg till m\u00e4tare" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sensork\u00e4lla" + } + } + } + }, + "title": "M\u00e4tare" } \ No newline at end of file From b62c0dcb32eef24be519c6da946f933a34813a64 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jul 2022 22:03:13 -0700 Subject: [PATCH 093/820] Guard invalid data sensor significant change (#74369) --- .../components/sensor/significant_change.py | 18 ++++++++++++++---- .../sensor/test_significant_change.py | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index 31b4f00c37f..6ff23b43508 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -63,13 +63,23 @@ def async_check_significant_change( absolute_change = 1.0 percentage_change = 2.0 + try: + # New state is invalid, don't report it + new_state_f = float(new_state) + except ValueError: + return False + + try: + # Old state was invalid, we should report again + old_state_f = float(old_state) + except ValueError: + return True + if absolute_change is not None and percentage_change is not None: return _absolute_and_relative_change( - float(old_state), float(new_state), absolute_change, percentage_change + old_state_f, new_state_f, absolute_change, percentage_change ) if absolute_change is not None: - return check_absolute_change( - float(old_state), float(new_state), absolute_change - ) + return check_absolute_change(old_state_f, new_state_f, absolute_change) return None diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index 051a92f3b07..bfa01d6eb08 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -52,6 +52,8 @@ TEMP_FREEDOM_ATTRS = { ("12.1", "12.2", TEMP_CELSIUS_ATTRS, False), ("70", "71", TEMP_FREEDOM_ATTRS, True), ("70", "70.5", TEMP_FREEDOM_ATTRS, False), + ("fail", "70", TEMP_FREEDOM_ATTRS, True), + ("70", "fail", TEMP_FREEDOM_ATTRS, False), ], ) async def test_significant_change_temperature(old_state, new_state, attrs, result): From 810b2a2bd6e281f45d7b58f4faee35d4d4e45f4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 00:36:42 -0500 Subject: [PATCH 094/820] Inline building entity registry dict (#74378) --- .../components/config/entity_registry.py | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index e6b91ee5a50..5f484c10472 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -29,7 +29,22 @@ def websocket_list_entities(hass, connection, msg): registry = er.async_get(hass) connection.send_message( websocket_api.result_message( - msg["id"], [_entry_dict(entry) for entry in registry.entities.values()] + msg["id"], + [ + { + "area_id": entry.area_id, + "config_entry_id": entry.config_entry_id, + "device_id": entry.device_id, + "disabled_by": entry.disabled_by, + "entity_category": entry.entity_category, + "entity_id": entry.entity_id, + "hidden_by": entry.hidden_by, + "icon": entry.icon, + "name": entry.name, + "platform": entry.platform, + } + for entry in registry.entities.values() + ], ) ) @@ -196,7 +211,7 @@ def websocket_remove_entity(hass, connection, msg): @callback -def _entry_dict(entry): +def _entry_ext_dict(entry): """Convert entry to API format.""" return { "area_id": entry.area_id, @@ -209,19 +224,12 @@ def _entry_dict(entry): "icon": entry.icon, "name": entry.name, "platform": entry.platform, + "capabilities": entry.capabilities, + "device_class": entry.device_class, + "has_entity_name": entry.has_entity_name, + "options": entry.options, + "original_device_class": entry.original_device_class, + "original_icon": entry.original_icon, + "original_name": entry.original_name, + "unique_id": entry.unique_id, } - - -@callback -def _entry_ext_dict(entry): - """Convert entry to API format.""" - data = _entry_dict(entry) - data["capabilities"] = entry.capabilities - data["device_class"] = entry.device_class - data["has_entity_name"] = entry.has_entity_name - data["options"] = entry.options - data["original_device_class"] = entry.original_device_class - data["original_icon"] = entry.original_icon - data["original_name"] = entry.original_name - data["unique_id"] = entry.unique_id - return data From 07f677e9e8fda1bf1e008da4a3f780261f8f7736 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 10:47:59 +0200 Subject: [PATCH 095/820] Migrate knx weather to native_* (#74386) --- homeassistant/components/knx/weather.py | 39 ++++++++++++------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 6e71c09501f..32f37ad2ac2 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -6,7 +6,14 @@ from xknx.devices import Weather as XknxWeather from homeassistant import config_entries from homeassistant.components.weather import WeatherEntity -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS, Platform +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + PRESSURE_PA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -68,7 +75,9 @@ class KNXWeather(KnxEntity, WeatherEntity): """Representation of a KNX weather device.""" _device: XknxWeather - _attr_temperature_unit = TEMP_CELSIUS + _attr_native_pressure_unit = PRESSURE_PA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" @@ -77,19 +86,14 @@ class KNXWeather(KnxEntity, WeatherEntity): self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @property - def temperature(self) -> float | None: - """Return current temperature.""" + def native_temperature(self) -> float | None: + """Return current temperature in C.""" return self._device.temperature @property - def pressure(self) -> float | None: - """Return current air pressure.""" - # KNX returns pA - HA requires hPa - return ( - self._device.air_pressure / 100 - if self._device.air_pressure is not None - else None - ) + def native_pressure(self) -> float | None: + """Return current air pressure in Pa.""" + return self._device.air_pressure @property def condition(self) -> str: @@ -107,11 +111,6 @@ class KNXWeather(KnxEntity, WeatherEntity): return self._device.wind_bearing @property - def wind_speed(self) -> float | None: - """Return current wind speed in km/h.""" - # KNX only supports wind speed in m/s - return ( - self._device.wind_speed * 3.6 - if self._device.wind_speed is not None - else None - ) + def native_wind_speed(self) -> float | None: + """Return current wind speed in m/s.""" + return self._device.wind_speed From 227d8b69a7e1cde0d1646bebbeb52ef1c18a125e Mon Sep 17 00:00:00 2001 From: Thanasis Date: Mon, 4 Jul 2022 14:11:36 +0300 Subject: [PATCH 096/820] Allowing for TOON cost sensors to work with Energy (#74315) --- homeassistant/components/toon/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 62829bf20ad..371672e184e 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -182,6 +182,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Gas Cost Today", section="gas_usage", measurement="day_cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=CURRENCY_EUR, icon="mdi:gas-cylinder", cls=ToonGasMeterDeviceSensor, @@ -230,6 +232,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Energy Cost Today", section="power_usage", measurement="day_cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=CURRENCY_EUR, icon="mdi:power-plug", cls=ToonElectricityMeterDeviceSensor, @@ -350,6 +354,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Water Cost Today", section="water_usage", measurement="day_cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=CURRENCY_EUR, icon="mdi:water-pump", entity_registry_enabled_default=False, From f851877449a66cf88b6db77396046a6cb4c9e25a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 4 Jul 2022 14:14:27 +0300 Subject: [PATCH 097/820] Bump aioimaplib to 1.0.0 (#74393) --- homeassistant/components/imap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 655590005bf..f4bbadfa6ac 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -2,7 +2,7 @@ "domain": "imap", "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", - "requirements": ["aioimaplib==0.9.0"], + "requirements": ["aioimaplib==1.0.0"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["aioimaplib"] diff --git a/requirements_all.txt b/requirements_all.txt index 87123abeba7..b5798a96679 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aiohttp_cors==0.7.0 aiohue==4.4.2 # homeassistant.components.imap -aioimaplib==0.9.0 +aioimaplib==1.0.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 From b5387ed769be041dc374e95b6bfd26830ef06245 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 4 Jul 2022 13:16:01 +0200 Subject: [PATCH 098/820] Update Pillow to 9.2.0 (#74371) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index ec170733e44..766167b7af9 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==9.1.1"], + "requirements": ["pydoods==1.0.2", "pillow==9.2.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pydoods"] diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 5ef47f0c941..98ca2986a86 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["ha-av==10.0.0b4", "pillow==9.1.1"], + "requirements": ["ha-av==10.0.0b4", "pillow==9.2.0"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index d2e08b77b23..4f967dbcc89 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==9.1.1"], + "requirements": ["pillow==9.2.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index d12f5ed92fd..59d19bc785e 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==9.1.1"], + "requirements": ["pillow==9.2.0"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 6715d1ba6db..666cc0c93ce 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==9.1.1", "pyzbar==0.1.7"], + "requirements": ["pillow==9.2.0", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated", "loggers": ["pyzbar"] diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index caee1d72c38..09457fc4ca5 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==9.1.1"], + "requirements": ["pillow==9.2.0"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index e87e1d37304..400664079e2 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==9.1.1", "simplehound==0.3"], + "requirements": ["pillow==9.2.0", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling", "loggers": ["simplehound"] diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 42d0eae1ecd..dd88fd7e277 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.23.0", - "pillow==9.1.1" + "pillow==9.2.0" ], "codeowners": [], "iot_class": "local_polling", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7ee5a9fe8d3..a62ae0b6fd1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ jinja2==3.1.2 lru-dict==1.1.7 orjson==3.7.5 paho-mqtt==1.6.1 -pillow==9.1.1 +pillow==9.2.0 pip>=21.0,<22.2 pyserial==3.5 python-slugify==4.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index b5798a96679..4e910c36412 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1239,7 +1239,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.1.1 +pillow==9.2.0 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b47aefebf16..fd3756a1c80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -850,7 +850,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.1.1 +pillow==9.2.0 # homeassistant.components.plex plexapi==4.11.2 From ab37f59345bd452186a074ce1635145cc0705670 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 13:38:53 +0200 Subject: [PATCH 099/820] Migrate meteoclimatic weather to native_* (#74392) --- .../components/meteoclimatic/weather.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 4faecdaa3ac..8044dd04aa8 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -3,7 +3,7 @@ from meteoclimatic import Condition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -38,6 +38,10 @@ async def async_setup_entry( class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, coordinator: DataUpdateCoordinator) -> None: """Initialise the weather platform.""" super().__init__(coordinator) @@ -71,27 +75,22 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): return format_condition(self.coordinator.data["weather"].condition) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data["weather"].temp_current - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self): """Return the humidity.""" return self.coordinator.data["weather"].humidity_current @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data["weather"].pressure_current @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data["weather"].wind_current From 0768ed453d7aa267ac853e481843c5515bb00ea9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 14:06:32 +0200 Subject: [PATCH 100/820] Migrate aemet to native_* (#74037) --- homeassistant/components/aemet/__init__.py | 39 ++++++++- homeassistant/components/aemet/const.py | 26 ++++-- homeassistant/components/aemet/sensor.py | 16 ++-- homeassistant/components/aemet/weather.py | 20 +++-- .../aemet/weather_update_coordinator.py | 20 ++--- tests/components/aemet/test_init.py | 84 +++++++++++++++++++ 6 files changed, 168 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index a914a23a0da..7b86a5559e0 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,18 +1,30 @@ """The AEMET OpenData component.""" +from __future__ import annotations + import logging +from typing import Any from aemet_opendata.interface import AEMET from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from .const import ( CONF_STATION_UPDATES, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + FORECAST_MODES, PLATFORMS, + RENAMED_FORECAST_SENSOR_KEYS, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -21,6 +33,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AEMET OpenData as config entry.""" + await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) + name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] @@ -60,3 +74,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +@callback +def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: + """Migrate AEMET entity entries. + + - Migrates unique ID from old forecast sensors to the new unique ID + """ + if entry.domain != Platform.SENSOR: + return None + for old_key, new_key in RENAMED_FORECAST_SENSOR_KEYS.items(): + for forecast_mode in FORECAST_MODES: + old_suffix = f"-forecast-{forecast_mode}-{old_key}" + if entry.unique_id.endswith(old_suffix): + new_suffix = f"-forecast-{forecast_mode}-{new_key}" + return { + "new_unique_id": entry.unique_id.replace(old_suffix, new_suffix) + } + + # No migration needed + return None diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 4be90011f5a..48e7335934f 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -18,6 +18,10 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -159,13 +163,13 @@ CONDITIONS_MAP = { FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, + ATTR_FORECAST_NATIVE_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -206,7 +210,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_FORECAST_NATIVE_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), @@ -216,13 +220,13 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_FORECAST_NATIVE_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_FORECAST_NATIVE_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, @@ -238,11 +242,17 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_SPEED, + key=ATTR_FORECAST_NATIVE_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), ) +RENAMED_FORECAST_SENSOR_KEYS = { + ATTR_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, +} WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_CONDITION, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f98e3fff49e..8439b166a47 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -45,17 +45,13 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - name_prefix, - unique_id_prefix, + f"{domain_data[ENTRY_NAME]} {mode} Forecast", + f"{unique_id}-forecast-{mode}", weather_coordinator, mode, description, ) for mode in FORECAST_MODES - if ( - (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") - and (unique_id_prefix := f"{unique_id}-forecast-{mode}") - ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -89,14 +85,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -113,7 +109,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -121,7 +117,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index d05442b621e..a67726d1f51 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,7 +1,12 @@ """Support for the AEMET OpenData service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -47,9 +52,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_temperature_unit = TEMP_CELSIUS - _attr_pressure_unit = PRESSURE_HPA - _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -83,12 +89,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -98,6 +104,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index c86465ea8f1..4f0bf6ac5ea 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -44,13 +44,13 @@ import async_timeout from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -406,10 +406,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_NATIVE_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_NATIVE_TEMP_LOW: self._get_temperature_low_day(day), ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed_day(day), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } @@ -421,13 +421,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return { ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(day, hour), ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_NATIVE_TEMP: self._get_temperature(day, hour), ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index b1f452c1b46..8dd177a145d 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -2,11 +2,15 @@ from unittest.mock import patch +import pytest import requests_mock from homeassistant.components.aemet.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .util import aemet_requests_mock @@ -42,3 +46,83 @@ async def test_unload_entry(hass): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "old_unique_id,new_unique_id", + [ + # Sensors which should be migrated + ( + "aemet_unique_id-forecast-daily-precipitation", + "aemet_unique_id-forecast-daily-native_precipitation", + ), + ( + "aemet_unique_id-forecast-daily-temperature", + "aemet_unique_id-forecast-daily-native_temperature", + ), + ( + "aemet_unique_id-forecast-daily-templow", + "aemet_unique_id-forecast-daily-native_templow", + ), + ( + "aemet_unique_id-forecast-daily-wind_speed", + "aemet_unique_id-forecast-daily-native_wind_speed", + ), + ( + "aemet_unique_id-forecast-hourly-precipitation", + "aemet_unique_id-forecast-hourly-native_precipitation", + ), + ( + "aemet_unique_id-forecast-hourly-temperature", + "aemet_unique_id-forecast-hourly-native_temperature", + ), + ( + "aemet_unique_id-forecast-hourly-templow", + "aemet_unique_id-forecast-hourly-native_templow", + ), + ( + "aemet_unique_id-forecast-hourly-wind_speed", + "aemet_unique_id-forecast-hourly-native_wind_speed", + ), + # Already migrated + ( + "aemet_unique_id-forecast-daily-native_templow", + "aemet_unique_id-forecast-daily-native_templow", + ), + # No migration needed + ( + "aemet_unique_id-forecast-daily-condition", + "aemet_unique_id-forecast-daily-condition", + ), + ], +) +async def test_migrate_unique_id_sensor( + hass: HomeAssistant, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test migration of unique_id.""" + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ), requests_mock.mock() as _m: + aemet_requests_mock(_m) + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + domain=SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=config_entry, + ) + assert entity.unique_id == old_unique_id + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == new_unique_id From 14e5001d0c69745c243582d7dfc6c2dbe2f54a23 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 14:10:25 +0200 Subject: [PATCH 101/820] Cleanup known_devices.yaml in device_tracker tests (#74404) --- tests/components/device_tracker/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index d8914032f36..3bafa59fb96 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -162,7 +162,7 @@ async def test_duplicate_mac_dev_id(mock_warning, hass): assert "Duplicate device IDs" in args[0], "Duplicate device IDs warning expected" -async def test_setup_without_yaml_file(hass, enable_custom_integrations): +async def test_setup_without_yaml_file(hass, yaml_devices, enable_custom_integrations): """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) From 1c0ece6ac1e051c9b0c78d36cd84ed1648537ffb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 14:20:47 +0200 Subject: [PATCH 102/820] Migrate met_eireann weather to native_* (#74391) Co-authored-by: avee87 <6134677+avee87@users.noreply.github.com> Co-authored-by: Franck Nijhof --- homeassistant/components/met_eireann/const.py | 20 +++---- .../components/met_eireann/weather.py | 59 +++++-------------- 2 files changed, 22 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py index 98d862183c4..efe80cb9d17 100644 --- a/homeassistant/components/met_eireann/const.py +++ b/homeassistant/components/met_eireann/const.py @@ -1,6 +1,4 @@ """Constants for Met Éireann component.""" -import logging - from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -12,13 +10,13 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, ) @@ -32,17 +30,15 @@ HOME_LOCATION_NAME = "Home" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}" -_LOGGER = logging.getLogger(".") - FORECAST_MAP = { ATTR_FORECAST_CONDITION: "condition", - ATTR_FORECAST_PRESSURE: "pressure", + ATTR_FORECAST_NATIVE_PRESSURE: "pressure", ATTR_FORECAST_PRECIPITATION: "precipitation", - ATTR_FORECAST_TEMP: "temperature", - ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_NATIVE_TEMP: "temperature", + ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_TIME: "datetime", ATTR_FORECAST_WIND_BEARING: "wind_bearing", - ATTR_FORECAST_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", } CONDITION_MAP = { diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index cbf5c99342a..f20f0e1254a 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -3,8 +3,6 @@ import logging from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, WeatherEntity, ) @@ -13,12 +11,9 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_INCHES, LENGTH_MILLIMETERS, PRESSURE_HPA, - PRESSURE_INHG, SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -27,9 +22,6 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed from .const import ATTRIBUTION, CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP @@ -54,12 +46,8 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( [ - MetEireannWeather( - coordinator, config_entry.data, hass.config.units.is_metric, False - ), - MetEireannWeather( - coordinator, config_entry.data, hass.config.units.is_metric, True - ), + MetEireannWeather(coordinator, config_entry.data, False), + MetEireannWeather(coordinator, config_entry.data, True), ] ) @@ -67,11 +55,15 @@ async def async_setup_entry( class MetEireannWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Éireann weather condition.""" - def __init__(self, coordinator, config, is_metric, hourly): + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + + def __init__(self, coordinator, config, hourly): """Initialise the platform with a data instance and site.""" super().__init__(coordinator) self._config = config - self._is_metric = is_metric self._hourly = hourly @property @@ -109,23 +101,14 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data.current_weather_data.get("temperature") @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" - pressure_hpa = self.coordinator.data.current_weather_data.get("pressure") - if self._is_metric or pressure_hpa is None: - return pressure_hpa - - return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) + return self.coordinator.data.current_weather_data.get("pressure") @property def humidity(self): @@ -133,16 +116,9 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.current_weather_data.get("humidity") @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - speed_m_s = self.coordinator.data.current_weather_data.get("wind_speed") - if self._is_metric or speed_m_s is None: - return speed_m_s - - speed_mi_h = convert_speed( - speed_m_s, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR - ) - return int(round(speed_mi_h)) + return self.coordinator.data.current_weather_data.get("wind_speed") @property def wind_bearing(self): @@ -161,7 +137,7 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): me_forecast = self.coordinator.data.hourly_forecast else: me_forecast = self.coordinator.data.daily_forecast - required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + required_keys = {"temperature", "datetime"} ha_forecast = [] @@ -171,13 +147,6 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): ha_item = { k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None } - if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: - precip_inches = convert_distance( - ha_item[ATTR_FORECAST_PRECIPITATION], - LENGTH_MILLIMETERS, - LENGTH_INCHES, - ) - ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From dd57d7d77f7d960966d04352269707c8f23e8e71 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 4 Jul 2022 14:24:21 +0200 Subject: [PATCH 103/820] Support unload for multiple adguard entries (#74360) --- homeassistant/components/adguard/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 1f2645e227c..2a244a5fe80 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -115,14 +115,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload AdGuard Home config entry.""" - hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) - hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) - hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) - hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) - hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) + hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) + hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_REFRESH) del hass.data[DOMAIN] return unload_ok From 6c3baf03aa8e1bb42ac76da1cc0b5afb222d2c41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 07:58:35 -0500 Subject: [PATCH 104/820] Make dispatcher setup lazy (#74374) --- homeassistant/helpers/dispatcher.py | 40 ++++++++++++---------- tests/components/cast/test_media_player.py | 12 ++++--- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index d1f7b2b97f9..12a6616b009 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -41,26 +41,13 @@ def async_dispatcher_connect( """ if DATA_DISPATCHER not in hass.data: hass.data[DATA_DISPATCHER] = {} - - job = HassJob( - catch_log_exception( - target, - lambda *args: "Exception in {} when dispatching '{}': {}".format( - # Functions wrapped in partial do not have a __name__ - getattr(target, "__name__", None) or str(target), - signal, - args, - ), - ) - ) - - hass.data[DATA_DISPATCHER].setdefault(signal, []).append(job) + hass.data[DATA_DISPATCHER].setdefault(signal, {})[target] = None @callback def async_remove_dispatcher() -> None: """Remove signal listener.""" try: - hass.data[DATA_DISPATCHER][signal].remove(job) + del hass.data[DATA_DISPATCHER][signal][target] except (KeyError, ValueError): # KeyError is key target listener did not exist # ValueError if listener did not exist within signal @@ -75,6 +62,21 @@ def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args) +def _generate_job(signal: str, target: Callable[..., Any]) -> HassJob: + """Generate a HassJob for a signal and target.""" + return HassJob( + catch_log_exception( + target, + lambda *args: "Exception in {} when dispatching '{}': {}".format( + # Functions wrapped in partial do not have a __name__ + getattr(target, "__name__", None) or str(target), + signal, + args, + ), + ) + ) + + @callback @bind_hass def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: @@ -82,7 +84,9 @@ def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: This method must be run in the event loop. """ - target_list = hass.data.get(DATA_DISPATCHER, {}).get(signal, []) - - for job in target_list: + target_list = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) + for target, job in target_list.items(): + if job is None: + job = _generate_job(signal, target) + target_list[target] = job hass.async_add_hass_job(job, *args) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 00626cc8c16..61a6067578a 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -311,7 +311,7 @@ async def test_internal_discovery_callback_fill_out_group_fail( await hass.async_block_till_done() # when called with incomplete info, it should use HTTP to get missing - discover = signal.mock_calls[0][1][0] + discover = signal.mock_calls[-1][1][0] assert discover == full_info get_multizone_status_mock.assert_called_once() @@ -352,7 +352,7 @@ async def test_internal_discovery_callback_fill_out_group( await hass.async_block_till_done() # when called with incomplete info, it should use HTTP to get missing - discover = signal.mock_calls[0][1][0] + discover = signal.mock_calls[-1][1][0] assert discover == full_info get_multizone_status_mock.assert_called_once() @@ -423,23 +423,25 @@ async def test_internal_discovery_callback_fill_out_cast_type_manufacturer( # when called with incomplete info, it should use HTTP to get missing get_cast_type_mock.assert_called_once() assert get_cast_type_mock.call_count == 1 - discover = signal.mock_calls[0][1][0] + discover = signal.mock_calls[2][1][0] assert discover == full_info assert "Fetched cast details for unknown model 'Chromecast'" in caplog.text + signal.reset_mock() # Call again, the model name should be fetched from cache discover_cast(FAKE_MDNS_SERVICE, info) await hass.async_block_till_done() assert get_cast_type_mock.call_count == 1 # No additional calls - discover = signal.mock_calls[1][1][0] + discover = signal.mock_calls[0][1][0] assert discover == full_info + signal.reset_mock() # Call for another model, need to call HTTP again get_cast_type_mock.return_value = full_info2.cast_info discover_cast(FAKE_MDNS_SERVICE, info2) await hass.async_block_till_done() assert get_cast_type_mock.call_count == 2 - discover = signal.mock_calls[2][1][0] + discover = signal.mock_calls[0][1][0] assert discover == full_info2 From 402a40c1080d33ad9921150833e6b0337fe1420a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 4 Jul 2022 14:59:24 +0200 Subject: [PATCH 105/820] Remove explicit use of mock_zeroconf in devolo Home Network (#74390) --- tests/components/devolo_home_network/test_init.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index 4f0c5b3fb58..1d15f337c17 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -12,7 +12,6 @@ from . import configure_integration @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_setup_entry(hass: HomeAssistant): """Test setup entry.""" entry = configure_integration(hass) @@ -24,7 +23,6 @@ async def test_setup_entry(hass: HomeAssistant): assert entry.state is ConfigEntryState.LOADED -@pytest.mark.usefixtures("mock_zeroconf") async def test_setup_device_not_found(hass: HomeAssistant): """Test setup entry.""" entry = configure_integration(hass) @@ -37,7 +35,6 @@ async def test_setup_device_not_found(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_unload_entry(hass: HomeAssistant): """Test unload entry.""" entry = configure_integration(hass) @@ -48,7 +45,6 @@ async def test_unload_entry(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_hass_stop(hass: HomeAssistant): """Test homeassistant stop event.""" entry = configure_integration(hass) From 5f5f1343cd231e6a98638096550066c64bd8daaf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 15:12:45 +0200 Subject: [PATCH 106/820] Migrate accuweather weather to native_* (#74407) --- .../components/accuweather/weather.py | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 536f66a3cb9..ae1824aef4a 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -6,19 +6,26 @@ from typing import Any, cast from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -66,19 +73,25 @@ class AccuWeatherEntity( ) -> None: """Initialize.""" super().__init__(coordinator) - self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL - wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][ - "Unit" - ] - if wind_speed_unit == "mi/h": - self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR + # Coordinator data is used also for sensors which don't have units automatically + # converted, hence the weather entity's native units follow the configured unit + # system + if coordinator.is_metric: + self._attr_native_precipitation_unit = LENGTH_MILLIMETERS + self._attr_native_pressure_unit = PRESSURE_HPA + self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_visibility_unit = LENGTH_KILOMETERS + self._attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + self._unit_system = API_METRIC else: - self._attr_wind_speed_unit = wind_speed_unit + self._unit_system = API_IMPERIAL + self._attr_native_precipitation_unit = LENGTH_INCHES + self._attr_native_pressure_unit = PRESSURE_INHG + self._attr_native_temperature_unit = TEMP_FAHRENHEIT + self._attr_native_visibility_unit = LENGTH_MILES + self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR self._attr_name = name self._attr_unique_id = coordinator.location_key - self._attr_temperature_unit = ( - TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT - ) self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -106,14 +119,14 @@ class AccuWeatherEntity( return None @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the temperature.""" return cast( float, self.coordinator.data["Temperature"][self._unit_system]["Value"] ) @property - def pressure(self) -> float: + def native_pressure(self) -> float: """Return the pressure.""" return cast( float, self.coordinator.data["Pressure"][self._unit_system]["Value"] @@ -125,7 +138,7 @@ class AccuWeatherEntity( return cast(int, self.coordinator.data["RelativeHumidity"]) @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return cast( float, self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"] @@ -137,7 +150,7 @@ class AccuWeatherEntity( return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"]) @property - def visibility(self) -> float: + def native_visibility(self) -> float: """Return the visibility.""" return cast( float, self.coordinator.data["Visibility"][self._unit_system]["Value"] @@ -162,9 +175,9 @@ class AccuWeatherEntity( return [ { ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), - ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"], - ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"], - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item), + ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"]["Value"], + ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"]["Value"], + ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(item), ATTR_FORECAST_PRECIPITATION_PROBABILITY: round( mean( [ @@ -173,7 +186,7 @@ class AccuWeatherEntity( ] ) ), - ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"], + ATTR_FORECAST_NATIVE_WIND_SPEED: item["WindDay"]["Speed"]["Value"], ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"], ATTR_FORECAST_CONDITION: [ k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v From 4e1359e2cc8676b9021d02164371a164b059727c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 15:18:57 +0200 Subject: [PATCH 107/820] Migrate ipma weather to native_* (#74387) --- homeassistant/components/ipma/weather.py | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 731c3d7fb60..dd585b88802 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -25,12 +25,12 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity, ) @@ -40,6 +40,8 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MODE, CONF_NAME, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -174,6 +176,10 @@ async def async_get_location(hass, api, latitude, longitude): class IPMAWeather(WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, location: Location, api: IPMA_API, config): """Initialise the platform with a data instance and station name.""" self._api = api @@ -237,7 +243,7 @@ class IPMAWeather(WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the current temperature.""" if not self._observation: return None @@ -245,7 +251,7 @@ class IPMAWeather(WeatherEntity): return self._observation.temperature @property - def pressure(self): + def native_pressure(self): """Return the current pressure.""" if not self._observation: return None @@ -261,7 +267,7 @@ class IPMAWeather(WeatherEntity): return self._observation.humidity @property - def wind_speed(self): + def native_wind_speed(self): """Return the current windspeed.""" if not self._observation: return None @@ -276,11 +282,6 @@ class IPMAWeather(WeatherEntity): return self._observation.wind_direction - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def forecast(self): """Return the forecast array.""" @@ -307,13 +308,13 @@ class IPMAWeather(WeatherEntity): ), None, ), - ATTR_FORECAST_TEMP: float(data_in.feels_like_temperature), + ATTR_FORECAST_NATIVE_TEMP: float(data_in.feels_like_temperature), ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( int(float(data_in.precipitation_probability)) if int(float(data_in.precipitation_probability)) >= 0 else None ), - ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } for data_in in forecast_filtered @@ -331,10 +332,10 @@ class IPMAWeather(WeatherEntity): ), None, ), - ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, - ATTR_FORECAST_TEMP: data_in.max_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.min_temperature, + ATTR_FORECAST_NATIVE_TEMP: data_in.max_temperature, ATTR_FORECAST_PRECIPITATION_PROBABILITY: data_in.precipitation_probability, - ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } for data_in in forecast_filtered From 8d0e54d7762ab664c5ed0836bb3e76546a60bea4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 08:41:23 -0500 Subject: [PATCH 108/820] Use the orjson equivalent default encoder when save_json is passed the default encoder (#74377) --- homeassistant/util/json.py | 27 +++++++++++++++++++++++++-- tests/util/test_json.py | 18 +++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index d69a4106728..68273c89743 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -11,6 +11,10 @@ import orjson from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.json import ( + JSONEncoder as DefaultHASSJSONEncoder, + json_encoder_default as default_hass_orjson_encoder, +) from .file import write_utf8_file, write_utf8_file_atomic @@ -52,6 +56,15 @@ def _orjson_encoder(data: Any) -> str: ).decode("utf-8") +def _orjson_default_encoder(data: Any) -> str: + """JSON encoder that uses orjson with hass defaults.""" + return orjson.dumps( + data, + option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS, + default=default_hass_orjson_encoder, + ).decode("utf-8") + + def save_json( filename: str, data: list | dict, @@ -64,10 +77,20 @@ def save_json( Returns True on success. """ - dump: Callable[[Any], Any] = json.dumps + dump: Callable[[Any], Any] try: if encoder: - json_data = json.dumps(data, indent=2, cls=encoder) + # For backwards compatibility, if they pass in the + # default json encoder we use _orjson_default_encoder + # which is the orjson equivalent to the default encoder. + if encoder is DefaultHASSJSONEncoder: + dump = _orjson_default_encoder + json_data = _orjson_default_encoder(data) + # If they pass a custom encoder that is not the + # DefaultHASSJSONEncoder, we use the slow path of json.dumps + else: + dump = json.dumps + json_data = json.dumps(data, indent=2, cls=encoder) else: dump = _orjson_encoder json_data = _orjson_encoder(data) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 9974cbb9628..28d321036c5 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -5,12 +5,13 @@ from json import JSONEncoder, dumps import math import os from tempfile import mkdtemp -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder from homeassistant.helpers.template import TupleWrapper from homeassistant.util.json import ( SerializationError, @@ -127,6 +128,21 @@ def test_custom_encoder(): assert data == "9" +def test_default_encoder_is_passed(): + """Test we use orjson if they pass in the default encoder.""" + fname = _path_for("test6") + with patch( + "homeassistant.util.json.orjson.dumps", return_value=b"{}" + ) as mock_orjson_dumps: + save_json(fname, {"any": 1}, encoder=DefaultHASSJSONEncoder) + assert len(mock_orjson_dumps.mock_calls) == 1 + # Patch json.dumps to make sure we are using the orjson path + with patch("homeassistant.util.json.json.dumps", side_effect=Exception): + save_json(fname, {"any": {1}}, encoder=DefaultHASSJSONEncoder) + data = load_json(fname) + assert data == {"any": [1]} + + def test_find_unserializable_data(): """Find unserializeable data.""" assert find_paths_unserializable_data(1) == {} From 18840c8af59bfd12c262ca1c6bb68a4cb5f0445c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:43:53 +0200 Subject: [PATCH 109/820] Add instance attributes to GeolocationEvent (#74389) --- .../components/geo_location/__init__.py | 25 +++++++++++-------- tests/components/geo_location/test_init.py | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index e58391ca84a..54542fa8503 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import final +from typing import Any, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE @@ -54,8 +54,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class GeolocationEvent(Entity): """Base class for an external event with an associated geolocation.""" + # Entity Properties + _attr_source: str + _attr_distance: float | None = None + _attr_latitude: float | None = None + _attr_longitude: float | None = None + + @final @property - def state(self): + def state(self) -> float | None: """Return the state of the sensor.""" if self.distance is not None: return round(self.distance, 1) @@ -64,32 +71,30 @@ class GeolocationEvent(Entity): @property def source(self) -> str: """Return source value of this external event.""" - raise NotImplementedError + return self._attr_source @property def distance(self) -> float | None: """Return distance value of this external event.""" - return None + return self._attr_distance @property def latitude(self) -> float | None: """Return latitude value of this external event.""" - return None + return self._attr_latitude @property def longitude(self) -> float | None: """Return longitude value of this external event.""" - return None + return self._attr_longitude @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return the state attributes of this external event.""" - data = {} + data: dict[str, Any] = {ATTR_SOURCE: self.source} if self.latitude is not None: data[ATTR_LATITUDE] = round(self.latitude, 5) if self.longitude is not None: data[ATTR_LONGITUDE] = round(self.longitude, 5) - if self.source is not None: - data[ATTR_SOURCE] = self.source return data diff --git a/tests/components/geo_location/test_init.py b/tests/components/geo_location/test_init.py index 00cb2a872d2..f2cb5c6b108 100644 --- a/tests/components/geo_location/test_init.py +++ b/tests/components/geo_location/test_init.py @@ -20,5 +20,5 @@ async def test_event(hass): assert entity.distance is None assert entity.latitude is None assert entity.longitude is None - with pytest.raises(NotImplementedError): + with pytest.raises(AttributeError): assert entity.source is None From 035e96a79b29e49537a685eaa811eb49de8da513 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:46:59 +0200 Subject: [PATCH 110/820] Remove system_health from mypy ignore list (#74415) --- .../components/system_health/__init__.py | 26 +++++++++++-------- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index be7232245e1..4821537fc8b 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable import dataclasses from datetime import datetime import logging +from typing import Any import aiohttp import async_timeout @@ -29,7 +30,7 @@ INFO_CALLBACK_TIMEOUT = 5 def async_register_info( hass: HomeAssistant, domain: str, - info_callback: Callable[[HomeAssistant], dict], + info_callback: Callable[[HomeAssistant], Awaitable[dict]], ): """Register an info callback. @@ -61,9 +62,10 @@ async def _register_system_health_platform(hass, integration_domain, platform): async def get_integration_info( hass: HomeAssistant, registration: SystemHealthRegistration -): +) -> dict[str, Any]: """Get integration system health.""" try: + assert registration.info_callback async with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): data = await registration.info_callback(hass) except asyncio.TimeoutError: @@ -72,7 +74,7 @@ async def get_integration_info( _LOGGER.exception("Error fetching info") data = {"error": {"type": "failed", "error": "unknown"}} - result = {"info": data} + result: dict[str, Any] = {"info": data} if registration.manage_url: result["manage_url"] = registration.manage_url @@ -88,15 +90,15 @@ def _format_value(val): return val -@websocket_api.async_response @websocket_api.websocket_command({vol.Required("type"): "system_health/info"}) +@websocket_api.async_response async def handle_info( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Handle an info request via a subscription.""" registrations: dict[str, SystemHealthRegistration] = hass.data[DOMAIN] data = {} - pending_info = {} + pending_info: dict[tuple[str, str], asyncio.Task] = {} for domain, domain_data in zip( registrations, @@ -138,7 +140,10 @@ async def handle_info( ) return - tasks = [asyncio.create_task(stop_event.wait()), *pending_info.values()] + tasks: set[asyncio.Task] = { + asyncio.create_task(stop_event.wait()), + *pending_info.values(), + } pending_lookup = {val: key for key, val in pending_info.items()} # One task is the stop_event.wait() and is always there @@ -160,8 +165,7 @@ async def handle_info( "key": key, } - if result.exception(): - exception = result.exception() + if exception := result.exception(): _LOGGER.error( "Error fetching system info for %s - %s", domain, @@ -206,7 +210,7 @@ class SystemHealthRegistration: async def async_check_can_reach_url( hass: HomeAssistant, url: str, more_info: str | None = None -) -> str: +) -> str | dict[str, str]: """Test if the url can be reached.""" session = aiohttp_client.async_get_clientsession(hass) diff --git a/mypy.ini b/mypy.ini index fb67983a31c..cab794e7bfe 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2900,9 +2900,6 @@ ignore_errors = true [mypy-homeassistant.components.sonos.statistics] ignore_errors = true -[mypy-homeassistant.components.system_health] -ignore_errors = true - [mypy-homeassistant.components.telegram_bot.polling] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index bbb628a76bb..94bb2682f3e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -112,7 +112,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.sensor", "homeassistant.components.sonos.speaker", "homeassistant.components.sonos.statistics", - "homeassistant.components.system_health", "homeassistant.components.telegram_bot.polling", "homeassistant.components.template.number", "homeassistant.components.template.sensor", From e3bd63934b9c31df55399cfb007d0895da464b8e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:47:35 +0200 Subject: [PATCH 111/820] Remove gree from mypy ignore list (#74411) --- homeassistant/components/gree/climate.py | 8 ++++---- mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 0d1c7d53f8b..2b6833dff2c 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -198,14 +198,14 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return TARGET_TEMPERATURE_STEP @property - def hvac_mode(self) -> str: + def hvac_mode(self) -> HVACMode | None: """Return the current HVAC mode for the device.""" if not self.coordinator.device.power: return HVACMode.OFF return HVAC_MODES.get(self.coordinator.device.mode) - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Invalid hvac_mode: {hvac_mode}") @@ -246,7 +246,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): self.async_write_ha_state() @property - def hvac_modes(self) -> list[str]: + def hvac_modes(self) -> list[HVACMode]: """Return the HVAC modes support by the device.""" modes = [*HVAC_MODES_REVERSE] modes.append(HVACMode.OFF) @@ -299,7 +299,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return PRESET_MODES @property - def fan_mode(self) -> str: + def fan_mode(self) -> str | None: """Return the current fan mode for the device.""" speed = self.coordinator.device.fan_speed return FAN_MODES.get(speed) diff --git a/mypy.ini b/mypy.ini index cab794e7bfe..22a6fd801c3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2663,12 +2663,6 @@ ignore_errors = true [mypy-homeassistant.components.google_assistant.trait] ignore_errors = true -[mypy-homeassistant.components.gree.climate] -ignore_errors = true - -[mypy-homeassistant.components.gree.switch] -ignore_errors = true - [mypy-homeassistant.components.harmony] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 94bb2682f3e..e9e78c2892b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -33,8 +33,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.google_assistant.http", "homeassistant.components.google_assistant.report_state", "homeassistant.components.google_assistant.trait", - "homeassistant.components.gree.climate", - "homeassistant.components.gree.switch", "homeassistant.components.harmony", "homeassistant.components.harmony.config_flow", "homeassistant.components.harmony.data", From fde829c4f0e3b0924bba34af051b2df8a885a90b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 16:59:36 +0200 Subject: [PATCH 112/820] Correct climacell weather migration to native_* (#74409) --- homeassistant/components/climacell/weather.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 2b284114981..6aee9b54f6c 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -10,13 +10,13 @@ from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -135,12 +135,12 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): data = { ATTR_FORECAST_TIME: forecast_dt.isoformat(), ATTR_FORECAST_CONDITION: translated_condition, - ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation, ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, - ATTR_FORECAST_TEMP: temp, - ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_NATIVE_TEMP: temp, + ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low, ATTR_FORECAST_WIND_BEARING: wind_direction, - ATTR_FORECAST_WIND_SPEED: wind_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, } return {k: v for k, v in data.items() if v is not None} @@ -224,7 +224,7 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): return CONDITIONS_V3[condition] @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" return self._get_cc_value( self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE From b082764e300970ba990bd3cf672e06a924b73eac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 10:36:56 -0500 Subject: [PATCH 113/820] Bump rflink to 0.0.63 (#74417) --- homeassistant/components/rflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index debc12ae4e0..6cef409a736 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -2,7 +2,7 @@ "domain": "rflink", "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": ["rflink==0.0.62"], + "requirements": ["rflink==0.0.63"], "codeowners": ["@javicalle"], "iot_class": "assumed_state", "loggers": ["rflink"] diff --git a/requirements_all.txt b/requirements_all.txt index 4e910c36412..2cae4c3846a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2080,7 +2080,7 @@ restrictedpython==5.2 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.62 +rflink==0.0.63 # homeassistant.components.ring ring_doorbell==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd3756a1c80..39bdf05db89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1391,7 +1391,7 @@ renault-api==0.1.11 restrictedpython==5.2 # homeassistant.components.rflink -rflink==0.0.62 +rflink==0.0.63 # homeassistant.components.ring ring_doorbell==0.7.2 From b3fec4c40147d98bc74a9047d8f34ba803ab5413 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Mon, 4 Jul 2022 17:12:41 +0100 Subject: [PATCH 114/820] Typehints and cleanup for metoffice (#74338) * Typehints and cleanup for metoffice * add myself as owner --- .strict-typing | 1 + CODEOWNERS | 4 +- .../components/metoffice/config_flow.py | 12 ++- homeassistant/components/metoffice/const.py | 2 +- homeassistant/components/metoffice/data.py | 16 ++-- homeassistant/components/metoffice/helpers.py | 14 ++-- .../components/metoffice/manifest.json | 2 +- homeassistant/components/metoffice/sensor.py | 43 ++++++----- homeassistant/components/metoffice/weather.py | 73 ++++++++++++------- mypy.ini | 11 +++ 10 files changed, 115 insertions(+), 63 deletions(-) diff --git a/.strict-typing b/.strict-typing index 1832a83641a..ebfed5dfa5b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -153,6 +153,7 @@ homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.media_source.* +homeassistant.components.metoffice.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* diff --git a/CODEOWNERS b/CODEOWNERS index 6ae1c9605db..9f53aeab34e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -630,8 +630,8 @@ build.json @home-assistant/supervisor /homeassistant/components/meteoalarm/ @rolfberkenbosch /homeassistant/components/meteoclimatic/ @adrianmo /tests/components/meteoclimatic/ @adrianmo -/homeassistant/components/metoffice/ @MrHarcombe -/tests/components/metoffice/ @MrHarcombe +/homeassistant/components/metoffice/ @MrHarcombe @avee87 +/tests/components/metoffice/ @MrHarcombe @avee87 /homeassistant/components/miflora/ @danielhiversen @basnijholt /homeassistant/components/mikrotik/ @engrbm87 /tests/components/mikrotik/ @engrbm87 diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py index 959680a90ec..3cf3b0fcda0 100644 --- a/homeassistant/components/metoffice/config_flow.py +++ b/homeassistant/components/metoffice/config_flow.py @@ -1,11 +1,15 @@ """Config flow for Met Office integration.""" +from __future__ import annotations + import logging +from typing import Any import datapoint import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -14,7 +18,9 @@ from .helpers import fetch_site _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate that the user input allows us to connect to DataPoint. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -40,7 +46,9 @@ class MetOfficeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index e413b102898..12f88cc6d56 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -37,7 +37,7 @@ MODE_3HOURLY_LABEL = "3-Hourly" MODE_DAILY = "daily" MODE_DAILY_LABEL = "Daily" -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLEAR_NIGHT: ["0"], ATTR_CONDITION_CLOUDY: ["7", "8"], ATTR_CONDITION_FOG: ["5", "6"], diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py index 607c09e90b6..4b2741ce0fb 100644 --- a/homeassistant/components/metoffice/data.py +++ b/homeassistant/components/metoffice/data.py @@ -1,11 +1,17 @@ """Common Met Office Data class used by both sensor and entity.""" +from dataclasses import dataclass + +from datapoint.Forecast import Forecast +from datapoint.Site import Site +from datapoint.Timestep import Timestep + + +@dataclass class MetOfficeData: """Data structure for MetOffice weather and forecast.""" - def __init__(self, now, forecast, site): - """Initialize the data object.""" - self.now = now - self.forecast = forecast - self.site = site + now: Forecast + forecast: list[Timestep] + site: Site diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 00d5e73501d..ecef7e5ddcb 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -1,8 +1,10 @@ """Helpers used for Met Office integration.""" +from __future__ import annotations import logging import datapoint +from datapoint.Site import Site from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.util.dt import utcnow @@ -13,7 +15,9 @@ from .data import MetOfficeData _LOGGER = logging.getLogger(__name__) -def fetch_site(connection: datapoint.Manager, latitude, longitude): +def fetch_site( + connection: datapoint.Manager, latitude: float, longitude: float +) -> Site | None: """Fetch site information from Datapoint API.""" try: return connection.get_nearest_forecast_site( @@ -24,7 +28,7 @@ def fetch_site(connection: datapoint.Manager, latitude, longitude): return None -def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: +def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOfficeData: """Fetch weather and forecast from Datapoint API.""" try: forecast = connection.get_forecast_for_site(site.id, mode) @@ -34,8 +38,8 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: else: time_now = utcnow() return MetOfficeData( - forecast.now(), - [ + now=forecast.now(), + forecast=[ timestep for day in forecast.days for timestep in day.timesteps @@ -44,5 +48,5 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: mode == MODE_3HOURLY or timestep.date.hour > 6 ) # ensures only one result per day in MODE_DAILY ], - site, + site=site, ) diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index d38d2d8cffe..887ecb3578d 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -3,7 +3,7 @@ "name": "Met Office", "documentation": "https://www.home-assistant.io/integrations/metoffice", "requirements": ["datapoint==0.9.8"], - "codeowners": ["@MrHarcombe"], + "codeowners": ["@MrHarcombe", "@avee87"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["datapoint"] diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 699b137c55f..e24e2299be4 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -1,6 +1,10 @@ """Support for UK Met Office weather service.""" from __future__ import annotations +from typing import Any + +from datapoint.Element import Element + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -17,7 +21,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import get_device_info from .const import ( @@ -34,6 +41,7 @@ from .const import ( VISIBILITY_CLASSES, VISIBILITY_DISTANCE_CLASSES, ) +from .data import MetOfficeData ATTR_LAST_UPDATE = "last_update" ATTR_SENSOR_ID = "sensor_id" @@ -170,21 +178,24 @@ async def async_setup_entry( ) -class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): +class MetOfficeCurrentSensor( + CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], SensorEntity +): """Implementation of a Met Office current weather condition sensor.""" def __init__( self, - coordinator, - hass_data, - use_3hourly, + coordinator: DataUpdateCoordinator[MetOfficeData], + hass_data: dict[str, Any], + use_3hourly: bool, description: SensorEntityDescription, - ): + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL + self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) @@ -192,11 +203,12 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{description.name}_{hass_data[METOFFICE_COORDINATES]}" if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" - - self.use_3hourly = use_3hourly + self._attr_entity_registry_enabled_default = ( + self.entity_description.entity_registry_enabled_default and use_3hourly + ) @property - def native_value(self): + def native_value(self) -> Any | None: """Return the state of the sensor.""" value = None @@ -224,13 +236,13 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): elif hasattr(self.coordinator.data.now, self.entity_description.key): value = getattr(self.coordinator.data.now, self.entity_description.key) - if hasattr(value, "value"): + if isinstance(value, Element): value = value.value return value @property - def icon(self): + def icon(self) -> str | None: """Return the icon for the entity card.""" value = self.entity_description.icon if self.entity_description.key == "weather": @@ -244,7 +256,7 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): return value @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, @@ -253,10 +265,3 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): ATTR_SITE_ID: self.coordinator.data.site.id, ATTR_SITE_NAME: self.coordinator.data.site.name, } - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return ( - self.entity_description.entity_registry_enabled_default and self.use_3hourly - ) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index f4e0bf61d30..184782d4c12 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,18 +1,27 @@ """Support for UK Met Office weather service.""" +from __future__ import annotations + +from typing import Any + +from datapoint.Timestep import Timestep + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import get_device_info from .const import ( @@ -28,6 +37,7 @@ from .const import ( MODE_DAILY, MODE_DAILY_LABEL, ) +from .data import MetOfficeData async def async_setup_entry( @@ -45,9 +55,8 @@ async def async_setup_entry( ) -def _build_forecast_data(timestep): - data = {} - data[ATTR_FORECAST_TIME] = timestep.date.isoformat() +def _build_forecast_data(timestep: Timestep) -> Forecast: + data = Forecast(datetime=timestep.date.isoformat()) if timestep.weather: data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value) if timestep.precipitation: @@ -61,21 +70,30 @@ def _build_forecast_data(timestep): return data -def _get_weather_condition(metoffice_code): +def _get_weather_condition(metoffice_code: str) -> str | None: for hass_name, metoffice_codes in CONDITION_CLASSES.items(): if metoffice_code in metoffice_codes: return hass_name return None -class MetOfficeWeather(CoordinatorEntity, WeatherEntity): +class MetOfficeWeather( + CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], WeatherEntity +): """Implementation of a Met Office weather condition.""" + _attr_attribution = ATTRIBUTION + _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_pressure_unit = PRESSURE_HPA _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR - def __init__(self, coordinator, hass_data, use_3hourly): + def __init__( + self, + coordinator: DataUpdateCoordinator[MetOfficeData], + hass_data: dict[str, Any], + use_3hourly: bool, + ) -> None: """Initialise the platform with a data instance.""" super().__init__(coordinator) @@ -89,62 +107,61 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" if self.coordinator.data.now: return _get_weather_condition(self.coordinator.data.now.weather.value) return None @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the platform temperature.""" - if self.coordinator.data.now.temperature: - return self.coordinator.data.now.temperature.value + weather_now = self.coordinator.data.now + if weather_now.temperature: + value = weather_now.temperature.value + return float(value) if value is not None else None return None @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the mean sea-level pressure.""" weather_now = self.coordinator.data.now if weather_now and weather_now.pressure: - return weather_now.pressure.value + value = weather_now.pressure.value + return float(value) if value is not None else None return None @property - def humidity(self): + def humidity(self) -> float | None: """Return the relative humidity.""" weather_now = self.coordinator.data.now if weather_now and weather_now.humidity: - return weather_now.humidity.value + value = weather_now.humidity.value + return float(value) if value is not None else None return None @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the wind speed.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_speed: - return weather_now.wind_speed.value + value = weather_now.wind_speed.value + return float(value) if value is not None else None return None @property - def wind_bearing(self): + def wind_bearing(self) -> str | None: """Return the wind bearing.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_direction: - return weather_now.wind_direction.value + value = weather_now.wind_direction.value + return str(value) if value is not None else None return None @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - if self.coordinator.data.forecast is None: - return None return [ _build_forecast_data(timestep) for timestep in self.coordinator.data.forecast ] - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION diff --git a/mypy.ini b/mypy.ini index 22a6fd801c3..77830664696 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1446,6 +1446,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.metoffice.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true From 1536936177cf685e8db073ba979443931f40cd18 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 20:39:14 +0200 Subject: [PATCH 115/820] Remove harmony from mypy ignore list (#74425) --- homeassistant/components/harmony/config_flow.py | 3 ++- homeassistant/components/harmony/data.py | 7 ++++--- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 16101f18cff..675acf600cb 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import logging +from typing import Any from urllib.parse import urlparse from aioharmony.hubconnector_websocket import HubConnector @@ -57,7 +58,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Harmony config flow.""" - self.harmony_config = {} + self.harmony_config: dict[str, Any] = {} async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index aa373d5813a..fbbbbd38e3a 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -21,13 +21,14 @@ _LOGGER = logging.getLogger(__name__) class HarmonyData(HarmonySubscriberMixin): """HarmonyData registers for Harmony hub updates.""" - def __init__(self, hass, address: str, name: str, unique_id: str): + _client: HarmonyClient + + def __init__(self, hass, address: str, name: str, unique_id: str | None) -> None: """Initialize a data object.""" super().__init__(hass) self._name = name self._unique_id = unique_id self._available = False - self._client = None self._address = address @property @@ -99,7 +100,7 @@ class HarmonyData(HarmonySubscriberMixin): configuration_url="https://www.logitech.com/en-us/my-account", ) - async def connect(self) -> bool: + async def connect(self) -> None: """Connect to the Harmony Hub.""" _LOGGER.debug("%s: Connecting", self._name) diff --git a/mypy.ini b/mypy.ini index 77830664696..e7897368189 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2674,15 +2674,6 @@ ignore_errors = true [mypy-homeassistant.components.google_assistant.trait] ignore_errors = true -[mypy-homeassistant.components.harmony] -ignore_errors = true - -[mypy-homeassistant.components.harmony.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.harmony.data] -ignore_errors = true - [mypy-homeassistant.components.hassio] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e9e78c2892b..f7b8bb1be2f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -33,9 +33,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.google_assistant.http", "homeassistant.components.google_assistant.report_state", "homeassistant.components.google_assistant.trait", - "homeassistant.components.harmony", - "homeassistant.components.harmony.config_flow", - "homeassistant.components.harmony.data", "homeassistant.components.hassio", "homeassistant.components.hassio.auth", "homeassistant.components.hassio.binary_sensor", From e02574c6d911b31d2edd8ef56d59ea6acdfafd21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 13:53:25 -0500 Subject: [PATCH 116/820] Bump pyunifiprotect to 4.0.9 (#74424) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index da82871d313..9aeb8b48050 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.8", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.9", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 2cae4c3846a..3c6a0ca8d63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.8 +pyunifiprotect==4.0.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39bdf05db89..18f38d1d230 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.8 +pyunifiprotect==4.0.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 02a0b8b6494780db0e41015a873a1c3e341f324d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 20:59:52 +0200 Subject: [PATCH 117/820] Add more type hints to demo (#74408) --- homeassistant/components/demo/geo_location.py | 23 ++++-- homeassistant/components/demo/humidifier.py | 10 ++- .../components/demo/image_processing.py | 21 ++--- homeassistant/components/demo/switch.py | 6 +- homeassistant/components/demo/vacuum.py | 77 ++++++++++--------- homeassistant/components/demo/weather.py | 71 ++++++++--------- 6 files changed, 109 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index fae626f37b2..27935300959 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -54,15 +54,15 @@ def setup_platform( class DemoManager: """Device manager for demo geolocation events.""" - def __init__(self, hass, add_entities): + def __init__(self, hass: HomeAssistant, add_entities: AddEntitiesCallback) -> None: """Initialise the demo geolocation event manager.""" self._hass = hass self._add_entities = add_entities - self._managed_devices = [] + self._managed_devices: list[DemoGeolocationEvent] = [] self._update(count=NUMBER_OF_DEMO_DEVICES) self._init_regular_updates() - def _generate_random_event(self): + def _generate_random_event(self) -> DemoGeolocationEvent: """Generate a random event in vicinity of this HA instance.""" home_latitude = self._hass.config.latitude home_longitude = self._hass.config.longitude @@ -83,13 +83,13 @@ class DemoManager: event_name, radius_in_km, latitude, longitude, LENGTH_KILOMETERS ) - def _init_regular_updates(self): + def _init_regular_updates(self) -> None: """Schedule regular updates based on configured time interval.""" track_time_interval( self._hass, lambda now: self._update(), DEFAULT_UPDATE_INTERVAL ) - def _update(self, count=1): + def _update(self, count: int = 1) -> None: """Remove events and add new random events.""" # Remove devices. for _ in range(1, count + 1): @@ -112,7 +112,14 @@ class DemoManager: class DemoGeolocationEvent(GeolocationEvent): """This represents a demo geolocation event.""" - def __init__(self, name, distance, latitude, longitude, unit_of_measurement): + def __init__( + self, + name: str, + distance: float, + latitude: float, + longitude: float, + unit_of_measurement: str, + ) -> None: """Initialize entity with data provided.""" self._name = name self._distance = distance @@ -131,7 +138,7 @@ class DemoGeolocationEvent(GeolocationEvent): return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo geolocation event.""" return False @@ -151,6 +158,6 @@ class DemoGeolocationEvent(GeolocationEvent): return self._longitude @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index c04c44cd8c5..c998a32ab55 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -1,6 +1,8 @@ """Demo platform that offers a fake humidifier device.""" from __future__ import annotations +from typing import Any + from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity from homeassistant.components.humidifier.const import HumidifierEntityFeature from homeassistant.config_entries import ConfigEntry @@ -78,22 +80,22 @@ class DemoHumidifier(HumidifierEntity): self._attr_available_modes = available_modes self._attr_device_class = device_class - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._attr_is_on = False self.async_write_ha_state() - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Set new humidity level.""" self._attr_target_humidity = humidity self.async_write_ha_state() - async def async_set_mode(self, mode): + async def async_set_mode(self, mode: str) -> None: """Update mode.""" self._attr_mode = mode self.async_write_ha_state() diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 070d8dcfa9c..58c884cc439 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,6 +1,7 @@ """Support for the demo image processing.""" from __future__ import annotations +from homeassistant.components.camera import Image from homeassistant.components.image_processing import ( ATTR_AGE, ATTR_CONFIDENCE, @@ -34,7 +35,7 @@ def setup_platform( class DemoImageProcessingAlpr(ImageProcessingAlprEntity): """Demo ALPR image processing entity.""" - def __init__(self, camera_entity, name): + def __init__(self, camera_entity: str, name: str) -> None: """Initialize demo ALPR image processing entity.""" super().__init__() @@ -42,21 +43,21 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): self._camera = camera_entity @property - def camera_entity(self): + def camera_entity(self) -> str: """Return camera entity id from process pictures.""" return self._camera @property - def confidence(self): + def confidence(self) -> int: """Return minimum confidence for send events.""" return 80 @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name - def process_image(self, image): + def process_image(self, image: Image) -> None: """Process image.""" demo_data = { "AC3829": 98.3, @@ -71,7 +72,7 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): class DemoImageProcessingFace(ImageProcessingFaceEntity): """Demo face identify image processing entity.""" - def __init__(self, camera_entity, name): + def __init__(self, camera_entity: str, name: str) -> None: """Initialize demo face image processing entity.""" super().__init__() @@ -79,21 +80,21 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity): self._camera = camera_entity @property - def camera_entity(self): + def camera_entity(self) -> str: """Return camera entity id from process pictures.""" return self._camera @property - def confidence(self): + def confidence(self) -> int: """Return minimum confidence for send events.""" return 80 @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name - def process_image(self, image): + def process_image(self, image: Image) -> None: """Process image.""" demo_data = [ { diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 217119e9372..2ad400ff3f7 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,6 +1,8 @@ """Demo platform that has two fake switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME @@ -69,12 +71,12 @@ class DemoSwitch(SwitchEntity): name=self.name, ) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._attr_is_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index feda379558b..58b76ba6347 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -1,6 +1,9 @@ """Demo platform for the vacuum component.""" from __future__ import annotations +from datetime import datetime +from typing import Any + from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, STATE_CLEANING, @@ -101,62 +104,62 @@ async def async_setup_platform( class DemoVacuum(VacuumEntity): """Representation of a demo vacuum.""" - def __init__(self, name, supported_features): + def __init__(self, name: str, supported_features: int) -> None: """Initialize the vacuum.""" self._name = name self._supported_features = supported_features self._state = False self._status = "Charging" self._fan_speed = FAN_SPEEDS[1] - self._cleaned_area = 0 + self._cleaned_area: float = 0 self._battery_level = 100 @property - def name(self): + def name(self) -> str: """Return the name of the vacuum.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo vacuum.""" return False @property - def is_on(self): + def is_on(self) -> bool: """Return true if vacuum is on.""" return self._state @property - def status(self): + def status(self) -> str: """Return the status of the vacuum.""" return self._status @property - def fan_speed(self): + def fan_speed(self) -> str: """Return the status of the vacuum.""" return self._fan_speed @property - def fan_speed_list(self): + def fan_speed_list(self) -> list[str]: """Return the status of the vacuum.""" return FAN_SPEEDS @property - def battery_level(self): + def battery_level(self) -> int: """Return the status of the vacuum.""" return max(0, min(100, self._battery_level)) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return self._supported_features - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the vacuum on.""" if self.supported_features & VacuumEntityFeature.TURN_ON == 0: return @@ -167,7 +170,7 @@ class DemoVacuum(VacuumEntity): self._status = "Cleaning" self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the vacuum off.""" if self.supported_features & VacuumEntityFeature.TURN_OFF == 0: return @@ -176,7 +179,7 @@ class DemoVacuum(VacuumEntity): self._status = "Charging" self.schedule_update_ha_state() - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the vacuum.""" if self.supported_features & VacuumEntityFeature.STOP == 0: return @@ -185,7 +188,7 @@ class DemoVacuum(VacuumEntity): self._status = "Stopping the current task" self.schedule_update_ha_state() - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" if self.supported_features & VacuumEntityFeature.CLEAN_SPOT == 0: return @@ -196,7 +199,7 @@ class DemoVacuum(VacuumEntity): self._status = "Cleaning spot" self.schedule_update_ha_state() - def locate(self, **kwargs): + def locate(self, **kwargs: Any) -> None: """Locate the vacuum (usually by playing a song).""" if self.supported_features & VacuumEntityFeature.LOCATE == 0: return @@ -204,7 +207,7 @@ class DemoVacuum(VacuumEntity): self._status = "Hi, I'm over here!" self.schedule_update_ha_state() - def start_pause(self, **kwargs): + def start_pause(self, **kwargs: Any) -> None: """Start, pause or resume the cleaning task.""" if self.supported_features & VacuumEntityFeature.PAUSE == 0: return @@ -218,7 +221,7 @@ class DemoVacuum(VacuumEntity): self._status = "Pausing the current task" self.schedule_update_ha_state() - def set_fan_speed(self, fan_speed, **kwargs): + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the vacuum's fan speed.""" if self.supported_features & VacuumEntityFeature.FAN_SPEED == 0: return @@ -227,7 +230,7 @@ class DemoVacuum(VacuumEntity): self._fan_speed = fan_speed self.schedule_update_ha_state() - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Tell the vacuum to return to its dock.""" if self.supported_features & VacuumEntityFeature.RETURN_HOME == 0: return @@ -237,7 +240,7 @@ class DemoVacuum(VacuumEntity): self._battery_level += 5 self.schedule_update_ha_state() - def send_command(self, command, params=None, **kwargs): + def send_command(self, command, params=None, **kwargs: Any) -> None: """Send a command to the vacuum.""" if self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0: return @@ -250,56 +253,56 @@ class DemoVacuum(VacuumEntity): class StateDemoVacuum(StateVacuumEntity): """Representation of a demo vacuum supporting states.""" - def __init__(self, name): + def __init__(self, name: str) -> None: """Initialize the vacuum.""" self._name = name self._supported_features = SUPPORT_STATE_SERVICES self._state = STATE_DOCKED self._fan_speed = FAN_SPEEDS[1] - self._cleaned_area = 0 + self._cleaned_area: float = 0 self._battery_level = 100 @property - def name(self): + def name(self) -> str: """Return the name of the vacuum.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo vacuum.""" return False @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return self._supported_features @property - def state(self): + def state(self) -> str: """Return the current state of the vacuum.""" return self._state @property - def battery_level(self): + def battery_level(self) -> int: """Return the current battery level of the vacuum.""" return max(0, min(100, self._battery_level)) @property - def fan_speed(self): + def fan_speed(self) -> str: """Return the current fan speed of the vacuum.""" return self._fan_speed @property - def fan_speed_list(self): + def fan_speed_list(self) -> list[str]: """Return the list of supported fan speeds.""" return FAN_SPEEDS @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} - def start(self): + def start(self) -> None: """Start or resume the cleaning task.""" if self.supported_features & VacuumEntityFeature.START == 0: return @@ -310,7 +313,7 @@ class StateDemoVacuum(StateVacuumEntity): self._battery_level -= 1 self.schedule_update_ha_state() - def pause(self): + def pause(self) -> None: """Pause the cleaning task.""" if self.supported_features & VacuumEntityFeature.PAUSE == 0: return @@ -319,7 +322,7 @@ class StateDemoVacuum(StateVacuumEntity): self._state = STATE_PAUSED self.schedule_update_ha_state() - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the cleaning task, do not return to dock.""" if self.supported_features & VacuumEntityFeature.STOP == 0: return @@ -327,7 +330,7 @@ class StateDemoVacuum(StateVacuumEntity): self._state = STATE_IDLE self.schedule_update_ha_state() - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Return dock to charging base.""" if self.supported_features & VacuumEntityFeature.RETURN_HOME == 0: return @@ -337,7 +340,7 @@ class StateDemoVacuum(StateVacuumEntity): event.call_later(self.hass, 30, self.__set_state_to_dock) - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" if self.supported_features & VacuumEntityFeature.CLEAN_SPOT == 0: return @@ -347,7 +350,7 @@ class StateDemoVacuum(StateVacuumEntity): self._battery_level -= 1 self.schedule_update_ha_state() - def set_fan_speed(self, fan_speed, **kwargs): + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the vacuum's fan speed.""" if self.supported_features & VacuumEntityFeature.FAN_SPEED == 0: return @@ -356,6 +359,6 @@ class StateDemoVacuum(StateVacuumEntity): self._fan_speed = fan_speed self.schedule_update_ha_state() - def __set_state_to_dock(self, _): + def __set_state_to_dock(self, _: datetime) -> None: self._state = STATE_DOCKED self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index eed3e970b12..dabaf8d066c 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -18,12 +18,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -40,7 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLOUDY: [], ATTR_CONDITION_FOG: [], ATTR_CONDITION_HAIL: [], @@ -125,17 +120,17 @@ class DemoWeather(WeatherEntity): def __init__( self, - name, - condition, - temperature, - humidity, - pressure, - wind_speed, - temperature_unit, - pressure_unit, - wind_speed_unit, - forecast, - ): + name: str, + condition: str, + temperature: float, + humidity: float, + pressure: float, + wind_speed: float, + temperature_unit: str, + pressure_unit: str, + wind_speed_unit: str, + forecast: list[list], + ) -> None: """Initialize the Demo weather.""" self._name = name self._condition = condition @@ -149,77 +144,77 @@ class DemoWeather(WeatherEntity): self._forecast = forecast @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"Demo Weather {self._name}" @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo weather condition.""" return False @property - def native_temperature(self): + def native_temperature(self) -> float: """Return the temperature.""" return self._native_temperature @property - def native_temperature_unit(self): + def native_temperature_unit(self) -> str: """Return the unit of measurement.""" return self._native_temperature_unit @property - def humidity(self): + def humidity(self) -> float: """Return the humidity.""" return self._humidity @property - def native_wind_speed(self): + def native_wind_speed(self) -> float: """Return the wind speed.""" return self._native_wind_speed @property - def native_wind_speed_unit(self): + def native_wind_speed_unit(self) -> str: """Return the wind speed.""" return self._native_wind_speed_unit @property - def native_pressure(self): + def native_pressure(self) -> float: """Return the pressure.""" return self._native_pressure @property - def native_pressure_unit(self): + def native_pressure_unit(self) -> str: """Return the pressure.""" return self._native_pressure_unit @property - def condition(self): + def condition(self) -> str: """Return the weather condition.""" return [ k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v ][0] @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return "Powered by Home Assistant" @property - def forecast(self): + def forecast(self) -> list[Forecast]: """Return the forecast.""" reftime = dt_util.now().replace(hour=16, minute=00) forecast_data = [] for entry in self._forecast: - data_dict = { - ATTR_FORECAST_TIME: reftime.isoformat(), - ATTR_FORECAST_CONDITION: entry[0], - ATTR_FORECAST_PRECIPITATION: entry[1], - ATTR_FORECAST_TEMP: entry[2], - ATTR_FORECAST_TEMP_LOW: entry[3], - ATTR_FORECAST_PRECIPITATION_PROBABILITY: entry[4], - } + data_dict = Forecast( + datetime=reftime.isoformat(), + condition=entry[0], + precipitation=entry[1], + temperature=entry[2], + templow=entry[3], + precipitation_probability=entry[4], + ) reftime = reftime + timedelta(hours=4) forecast_data.append(data_dict) From 560fbd1a0e685988ec2759dbe95fb1d19d86718a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:10:26 +0200 Subject: [PATCH 118/820] Remove lutron_caseta from mypy ignore list (#74427) --- homeassistant/components/lutron_caseta/__init__.py | 13 +++++++------ homeassistant/components/lutron_caseta/cover.py | 4 ++-- .../components/lutron_caseta/device_trigger.py | 4 +++- homeassistant/components/lutron_caseta/models.py | 4 +--- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 6 files changed, 13 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index f2225900aad..7faf70f0d7a 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -215,10 +215,10 @@ def _async_register_button_devices( config_entry_id: str, bridge_device, button_devices_by_id: dict[int, dict], -) -> dict[str, dr.DeviceEntry]: +) -> dict[str, dict]: """Register button devices (Pico Remotes) in the device registry.""" device_registry = dr.async_get(hass) - button_devices_by_dr_id = {} + button_devices_by_dr_id: dict[str, dict] = {} seen = set() for device in button_devices_by_id.values(): @@ -226,7 +226,7 @@ def _async_register_button_devices( continue seen.add(device["serial"]) area, name = _area_and_name_from_name(device["name"]) - device_args = { + device_args: dict[str, Any] = { "name": f"{area} {name}", "manufacturer": MANUFACTURER, "config_entry_id": config_entry_id, @@ -246,7 +246,8 @@ def _async_register_button_devices( def _area_and_name_from_name(device_name: str) -> tuple[str, str]: """Return the area and name from the devices internal name.""" if "_" in device_name: - return device_name.split("_", 1) + area_device_name = device_name.split("_", 1) + return area_device_name[0], area_device_name[1] return UNASSIGNED_AREA, device_name @@ -381,13 +382,13 @@ class LutronCasetaDevice(Entity): class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): """A lutron_caseta entity that can update by syncing data from the bridge.""" - async def async_update(self): + async def async_update(self) -> None: """Update when forcing a refresh of the device.""" self._device = self._smartbridge.get_device_by_id(self.device_id) _LOGGER.debug(self._device) -def _id_to_identifier(lutron_id: str) -> None: +def _id_to_identifier(lutron_id: str) -> tuple[str, str]: """Convert a lutron caseta identifier to a device identifier.""" return (DOMAIN, lutron_id) diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index b74642a8589..d63c1191d57 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -66,13 +66,13 @@ class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._smartbridge.lower_cover(self.device_id) - self.async_update() + await self.async_update() self.async_write_ha_state() async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._smartbridge.raise_cover(self.device_id) - self.async_update() + await self.async_update() self.async_write_ha_state() async def async_set_cover_position(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index dcdf5d584a4..d938ad6e7f2 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -430,9 +430,11 @@ async def async_attach_trigger( """Attach a trigger.""" device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) + assert device + assert device.model device_type = _device_model_to_type(device.model) _, serial = list(device.identifiers)[0] - schema = DEVICE_TYPE_SCHEMA_MAP.get(device_type) + schema = DEVICE_TYPE_SCHEMA_MAP[device_type] valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP[device_type] config = schema(config) event_config = { diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 5845c888a2e..362760b0caf 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -6,8 +6,6 @@ from typing import Any from pylutron_caseta.smartbridge import Smartbridge -from homeassistant.helpers.device_registry import DeviceEntry - @dataclass class LutronCasetaData: @@ -15,4 +13,4 @@ class LutronCasetaData: bridge: Smartbridge bridge_device: dict[str, Any] - button_devices: dict[str, DeviceEntry] + button_devices: dict[str, dict] diff --git a/mypy.ini b/mypy.ini index e7897368189..30bc97f545a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2752,15 +2752,6 @@ ignore_errors = true [mypy-homeassistant.components.lovelace.websocket] ignore_errors = true -[mypy-homeassistant.components.lutron_caseta] -ignore_errors = true - -[mypy-homeassistant.components.lutron_caseta.device_trigger] -ignore_errors = true - -[mypy-homeassistant.components.lutron_caseta.switch] -ignore_errors = true - [mypy-homeassistant.components.lyric.climate] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f7b8bb1be2f..939290ca9ab 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -59,9 +59,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", "homeassistant.components.lovelace.websocket", - "homeassistant.components.lutron_caseta", - "homeassistant.components.lutron_caseta.device_trigger", - "homeassistant.components.lutron_caseta.switch", "homeassistant.components.lyric.climate", "homeassistant.components.lyric.config_flow", "homeassistant.components.lyric.sensor", From a7da8673bf44a9126ec4c0f91e2736eb841b004d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:26:33 +0200 Subject: [PATCH 119/820] Use instance attributes in usgs_earthquakes_feed (#74403) --- .../usgs_earthquakes_feed/geo_location.py | 133 +++++++----------- 1 file changed, 51 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 6e3eb9b2337..c82705174fe 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -1,15 +1,19 @@ """Support for U.S. Geological Survey Earthquake Hazards Program Feeds.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import logging +from typing import Any from aio_geojson_usgs_earthquakes import UsgsEarthquakeHazardsProgramFeedManager +from aio_geojson_usgs_earthquakes.feed_entry import ( + UsgsEarthquakeHazardsProgramFeedEntry, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_TIME, CONF_LATITUDE, CONF_LONGITUDE, @@ -96,14 +100,14 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the USGS Earthquake Hazards Program Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - feed_type = config[CONF_FEED_TYPE] - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + feed_type: str = config[CONF_FEED_TYPE] + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE] + radius_in_km: float = config[CONF_RADIUS] + minimum_magnitude: float = config[CONF_MINIMUM_MAGNITUDE] # Initialize the entity manager. manager = UsgsEarthquakesFeedEntityManager( hass, @@ -128,14 +132,14 @@ class UsgsEarthquakesFeedEntityManager: def __init__( self, - hass, - async_add_entities, - scan_interval, - coordinates, - feed_type, - radius_in_km, - minimum_magnitude, - ): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + feed_type: str, + radius_in_km: float, + minimum_magnitude: float, + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass @@ -153,10 +157,10 @@ class UsgsEarthquakesFeedEntityManager: self._async_add_entities = async_add_entities self._scan_interval = scan_interval - async def async_init(self): + async def async_init(self) -> None: """Schedule initial and regular updates based on configured time interval.""" - async def update(event_time): + async def update(event_time: datetime) -> None: """Update.""" await self.async_update() @@ -164,26 +168,28 @@ class UsgsEarthquakesFeedEntityManager: async_track_time_interval(self._hass, update, self._scan_interval) _LOGGER.debug("Feed entity manager initialized") - async def async_update(self): + async def async_update(self) -> None: """Refresh data.""" await self._feed_manager.update() _LOGGER.debug("Feed entity manager updated") - def get_entry(self, external_id): + def get_entry( + self, external_id: str + ) -> UsgsEarthquakeHazardsProgramFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - async def _generate_entity(self, external_id): + async def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = UsgsEarthquakesEvent(self, external_id) # Add new entities to HA. self._async_add_entities([new_entity], True) - async def _update_entity(self, external_id): + async def _update_entity(self, external_id: str) -> None: """Update entity.""" async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - async def _remove_entity(self, external_id): + async def _remove_entity(self, external_id: str) -> None: """Remove entity.""" async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) @@ -191,15 +197,17 @@ class UsgsEarthquakesFeedEntityManager: class UsgsEarthquakesEvent(GeolocationEvent): """This represents an external event with USGS Earthquake data.""" - def __init__(self, feed_manager, external_id): + _attr_icon = "mdi:pulse" + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = DEFAULT_UNIT_OF_MEASUREMENT + + def __init__( + self, feed_manager: UsgsEarthquakesFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._place = None self._magnitude = None self._time = None @@ -207,10 +215,10 @@ class UsgsEarthquakesEvent(GeolocationEvent): self._status = None self._type = None self._alert = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -224,36 +232,33 @@ class UsgsEarthquakesEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for USGS Earthquake events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed( + self, feed_entry: UsgsEarthquakeHazardsProgramFeedEntry + ) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._place = feed_entry.place self._magnitude = feed_entry.magnitude self._time = feed_entry.time @@ -263,42 +268,7 @@ class UsgsEarthquakesEvent(GeolocationEvent): self._alert = feed_entry.alert @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return DEFAULT_UNIT_OF_MEASUREMENT - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( @@ -310,7 +280,6 @@ class UsgsEarthquakesEvent(GeolocationEvent): (ATTR_STATUS, self._status), (ATTR_TYPE, self._type), (ATTR_ALERT, self._alert), - (ATTR_ATTRIBUTION, self._attribution), ): if value or isinstance(value, bool): attributes[key] = value From dd6725b80a5efebda36c64c78250f3374e85c3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 5 Jul 2022 00:04:56 +0300 Subject: [PATCH 120/820] Replace pylint-strict-informational with `fail-on=I` (#74311) `fail-on` is available since pylint 2.9.0. https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#fail-on https://github.com/PyCQA/pylint/issues/3251#issuecomment-1170941337 Co-authored-by: J. Nick Koston --- pyproject.toml | 4 +++- requirements_test.txt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f50c835e58f..51f1d115e7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,6 @@ init-hook = """\ load-plugins = [ "pylint.extensions.code_style", "pylint.extensions.typing", - "pylint_strict_informational", "hass_constructor", "hass_enforce_type_hints", "hass_imports", @@ -123,6 +122,9 @@ extension-pkg-allow-list = [ "orjson", "cv2", ] +fail-on = [ + "I", +] [tool.pylint.BASIC] class-const-naming-style = "any" diff --git a/requirements_test.txt b/requirements_test.txt index 3172c1f5544..113794bda1c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,7 +15,6 @@ mypy==0.961 pre-commit==2.19.0 pylint==2.14.4 pipdeptree==2.2.1 -pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==3.0.0 pytest-freezegun==0.4.2 From ebc8fba5bfc413a293f1cd71fc67e870304398c6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Jul 2022 00:23:09 +0000 Subject: [PATCH 121/820] [ci skip] Translation update --- .../components/anthemav/translations/pl.json | 19 ++++++++++++++ .../components/awair/translations/pl.json | 7 ++++++ .../components/esphome/translations/pl.json | 4 +-- .../components/generic/translations/ca.json | 4 +++ .../components/generic/translations/de.json | 4 +++ .../components/generic/translations/el.json | 4 +++ .../components/generic/translations/et.json | 4 +++ .../components/generic/translations/hu.json | 4 +++ .../components/generic/translations/ja.json | 4 +++ .../components/generic/translations/pl.json | 4 +++ .../generic/translations/pt-BR.json | 4 +++ .../generic/translations/zh-Hant.json | 4 +++ .../components/hive/translations/pl.json | 9 ++++++- .../homeassistant/translations/ca.json | 1 + .../homeassistant/translations/et.json | 1 + .../homeassistant/translations/hu.json | 1 + .../components/lcn/translations/pl.json | 1 + .../lg_soundbar/translations/pl.json | 18 +++++++++++++ .../components/life360/translations/pl.json | 25 ++++++++++++++++++- .../components/nextdns/translations/ca.json | 24 ++++++++++++++++++ .../components/nextdns/translations/de.json | 24 ++++++++++++++++++ .../components/nextdns/translations/el.json | 24 ++++++++++++++++++ .../components/nextdns/translations/et.json | 24 ++++++++++++++++++ .../components/nextdns/translations/hu.json | 24 ++++++++++++++++++ .../components/nextdns/translations/ja.json | 24 ++++++++++++++++++ .../nextdns/translations/pt-BR.json | 24 ++++++++++++++++++ .../nextdns/translations/zh-Hant.json | 24 ++++++++++++++++++ .../components/nina/translations/pl.json | 22 ++++++++++++++++ .../components/qnap_qsw/translations/pl.json | 6 +++++ .../simplepush/translations/pl.json | 21 ++++++++++++++++ .../soundtouch/translations/pl.json | 21 ++++++++++++++++ 31 files changed, 380 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/pl.json create mode 100644 homeassistant/components/lg_soundbar/translations/pl.json create mode 100644 homeassistant/components/nextdns/translations/ca.json create mode 100644 homeassistant/components/nextdns/translations/de.json create mode 100644 homeassistant/components/nextdns/translations/el.json create mode 100644 homeassistant/components/nextdns/translations/et.json create mode 100644 homeassistant/components/nextdns/translations/hu.json create mode 100644 homeassistant/components/nextdns/translations/ja.json create mode 100644 homeassistant/components/nextdns/translations/pt-BR.json create mode 100644 homeassistant/components/nextdns/translations/zh-Hant.json create mode 100644 homeassistant/components/simplepush/translations/pl.json create mode 100644 homeassistant/components/soundtouch/translations/pl.json diff --git a/homeassistant/components/anthemav/translations/pl.json b/homeassistant/components/anthemav/translations/pl.json new file mode 100644 index 00000000000..18eaecb9845 --- /dev/null +++ b/homeassistant/components/anthemav/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "cannot_receive_deviceinfo": "Nie uda\u0142o si\u0119 pobra\u0107 adresu MAC. Upewnij si\u0119, \u017ce urz\u0105dzenie jest w\u0142\u0105czone." + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index 38bd24f714b..b0ee9e85adf 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -17,6 +17,13 @@ }, "description": "Wprowad\u017a ponownie token dost\u0119pu programisty Awair." }, + "reauth_confirm": { + "data": { + "access_token": "Token dost\u0119pu", + "email": "Adres e-mail" + }, + "description": "Wprowad\u017a ponownie token dost\u0119pu programisty Awair." + }, "user": { "data": { "access_token": "Token dost\u0119pu", diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index da6154e9fb6..dd495f0f097 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -9,7 +9,7 @@ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_psk": "Klucz szyfruj\u0105cy transport jest nieprawid\u0142owy. Upewnij si\u0119, \u017ce pasuje do tego, kt\u00f3ry masz w swojej konfiguracji.", - "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Nazwa hosta lub adres IP", "port": "Port" }, - "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia [ESPHome](https://esphomelib.com/) w\u0119z\u0142a." + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia w\u0119z\u0142a [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json index 818a030c8a7..90e12b8ea69 100644 --- a/homeassistant/components/generic/translations/ca.json +++ b/homeassistant/components/generic/translations/ca.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Ja hi ha una c\u00e0mera amb aquest URL de configuraci\u00f3.", "invalid_still_image": "L'URL no ha retornat una imatge fixa v\u00e0lida", + "malformed_url": "URL mal format", "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", + "relative_url": "Els URL relatius no s'admeten", "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Ja hi ha una c\u00e0mera amb aquest URL de configuraci\u00f3.", "invalid_still_image": "L'URL no ha retornat una imatge fixa v\u00e0lida", + "malformed_url": "URL mal format", "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", + "relative_url": "Els URL relatius no s'admeten", "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json index 0c14e95a683..57d15a8efea 100644 --- a/homeassistant/components/generic/translations/de.json +++ b/homeassistant/components/generic/translations/de.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Es existiert bereits eine Kamera mit diesen URL-Einstellungen.", "invalid_still_image": "URL hat kein g\u00fcltiges Standbild zur\u00fcckgegeben", + "malformed_url": "Falsch formatierte URL", "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", + "relative_url": "Relative URLs sind nicht zul\u00e4ssig", "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Es existiert bereits eine Kamera mit diesen URL-Einstellungen.", "invalid_still_image": "URL hat kein g\u00fcltiges Standbild zur\u00fcckgegeben", + "malformed_url": "Falsch formatierte URL", "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", + "relative_url": "Relative URLs sind nicht zul\u00e4ssig", "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", diff --git a/homeassistant/components/generic/translations/el.json b/homeassistant/components/generic/translations/el.json index f97714a53c1..29063cdc216 100644 --- a/homeassistant/components/generic/translations/el.json +++ b/homeassistant/components/generic/translations/el.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 URL.", "invalid_still_image": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1", + "malformed_url": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL", "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", + "relative_url": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 URL", "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", @@ -50,7 +52,9 @@ "error": { "already_exists": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 URL.", "invalid_still_image": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1", + "malformed_url": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL", "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", + "relative_url": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 URL", "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json index a50e4d4aaa7..2746f84b9a1 100644 --- a/homeassistant/components/generic/translations/et.json +++ b/homeassistant/components/generic/translations/et.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Nende URL-i seadetega kaamera on juba olemas.", "invalid_still_image": "URL ei tagastanud kehtivat pilti", + "malformed_url": "Vigane URL", "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", + "relative_url": "Osalised URL-id pole lubatud", "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Nende URL-i seadetega kaamera on juba olemas.", "invalid_still_image": "URL ei tagastanud kehtivat pilti", + "malformed_url": "Vigane URL", "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", + "relative_url": "Osalised URL-id pole lubatud", "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", diff --git a/homeassistant/components/generic/translations/hu.json b/homeassistant/components/generic/translations/hu.json index 76992a1fde7..bf03ec88d96 100644 --- a/homeassistant/components/generic/translations/hu.json +++ b/homeassistant/components/generic/translations/hu.json @@ -7,7 +7,9 @@ "error": { "already_exists": "M\u00e1r l\u00e9tezik egy kamera ezekkel az URL-be\u00e1ll\u00edt\u00e1sokkal.", "invalid_still_image": "Az URL nem adott vissza \u00e9rv\u00e9nyes \u00e1ll\u00f3k\u00e9pet", + "malformed_url": "Hib\u00e1s URL", "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", + "relative_url": "A relat\u00edv URL-ek nem enged\u00e9lyezettek", "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", @@ -50,7 +52,9 @@ "error": { "already_exists": "M\u00e1r l\u00e9tezik egy kamera ezekkel az URL-be\u00e1ll\u00edt\u00e1sokkal.", "invalid_still_image": "Az URL nem adott vissza \u00e9rv\u00e9nyes \u00e1ll\u00f3k\u00e9pet", + "malformed_url": "Hib\u00e1s URL", "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", + "relative_url": "A relat\u00edv URL-ek nem enged\u00e9lyezettek", "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index f4fd7d8ec46..499de6409e6 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", "invalid_still_image": "URL\u304c\u6709\u52b9\u306a\u9759\u6b62\u753b\u50cf\u3092\u8fd4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", + "malformed_url": "\u4e0d\u6b63\u306a\u5f62\u5f0f\u306eURL", "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "relative_url": "\u76f8\u5bfeURL(Relative URLs)\u306f\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", @@ -50,7 +52,9 @@ "error": { "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", "invalid_still_image": "URL\u304c\u6709\u52b9\u306a\u9759\u6b62\u753b\u50cf\u3092\u8fd4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", + "malformed_url": "\u4e0d\u6b63\u306a\u5f62\u5f0f\u306eURL", "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "relative_url": "\u76f8\u5bfeURL(Relative URLs)\u306f\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json index 81817faf236..71c50148957 100644 --- a/homeassistant/components/generic/translations/pl.json +++ b/homeassistant/components/generic/translations/pl.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Kamera z tymi ustawieniami adresu URL ju\u017c istnieje.", "invalid_still_image": "Adres URL nie zwr\u00f3ci\u0142 prawid\u0142owego obrazu nieruchomego (still image)", + "malformed_url": "Nieprawid\u0142owy adres URL", "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", + "relative_url": "Wzgl\u0119dne adresy URL s\u0105 niedozwolone", "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Kamera z tymi ustawieniami adresu URL ju\u017c istnieje.", "invalid_still_image": "Adres URL nie zwr\u00f3ci\u0142 prawid\u0142owego obrazu nieruchomego (still image)", + "malformed_url": "Nieprawid\u0142owy adres URL", "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", + "relative_url": "Wzgl\u0119dne adresy URL s\u0105 niedozwolone", "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json index 1a61cdeac97..86ac7a01efb 100644 --- a/homeassistant/components/generic/translations/pt-BR.json +++ b/homeassistant/components/generic/translations/pt-BR.json @@ -7,7 +7,9 @@ "error": { "already_exists": "J\u00e1 existe uma c\u00e2mera com essas configura\u00e7\u00f5es de URL.", "invalid_still_image": "A URL n\u00e3o retornou uma imagem est\u00e1tica v\u00e1lida", + "malformed_url": "URL malformada", "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", + "relative_url": "URLs relativas n\u00e3o s\u00e3o permitidas", "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", @@ -50,7 +52,9 @@ "error": { "already_exists": "J\u00e1 existe uma c\u00e2mera com essas configura\u00e7\u00f5es de URL.", "invalid_still_image": "A URL n\u00e3o retornou uma imagem est\u00e1tica v\u00e1lida", + "malformed_url": "URL malformada", "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", + "relative_url": "URLs relativas n\u00e3o s\u00e3o permitidas", "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json index 595bf019f64..ded2ea569c4 100644 --- a/homeassistant/components/generic/translations/zh-Hant.json +++ b/homeassistant/components/generic/translations/zh-Hant.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u5df2\u7d93\u6709\u4f7f\u7528\u76f8\u540c URL \u7684\u651d\u5f71\u6a5f\u3002", "invalid_still_image": "URL \u56de\u50b3\u4e26\u975e\u6709\u6548\u975c\u614b\u5f71\u50cf", + "malformed_url": "URL \u683c\u5f0f\u932f\u8aa4", "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", + "relative_url": "\u4e0d\u5141\u8a31\u4f7f\u7528\u76f8\u5c0d\u61c9 URL", "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", @@ -50,7 +52,9 @@ "error": { "already_exists": "\u5df2\u7d93\u6709\u4f7f\u7528\u76f8\u540c URL \u7684\u651d\u5f71\u6a5f\u3002", "invalid_still_image": "URL \u56de\u50b3\u4e26\u975e\u6709\u6548\u975c\u614b\u5f71\u50cf", + "malformed_url": "URL \u683c\u5f0f\u932f\u8aa4", "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", + "relative_url": "\u4e0d\u5141\u8a31\u4f7f\u7528\u76f8\u5c0d\u61c9 URL", "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", diff --git a/homeassistant/components/hive/translations/pl.json b/homeassistant/components/hive/translations/pl.json index 0c61fa74feb..b227e5b1e9a 100644 --- a/homeassistant/components/hive/translations/pl.json +++ b/homeassistant/components/hive/translations/pl.json @@ -20,6 +20,13 @@ "description": "Wprowad\u017a sw\u00f3j kod uwierzytelniaj\u0105cy Hive. \n\nWprowad\u017a kod 0000, aby poprosi\u0107 o kolejny kod.", "title": "Uwierzytelnianie dwusk\u0142adnikowe Hive" }, + "configuration": { + "data": { + "device_name": "Nazwa urz\u0105dzenia" + }, + "description": "Wprowad\u017a konfiguracj\u0119 Hive", + "title": "Konfiguracja Hive." + }, "reauth": { "data": { "password": "Has\u0142o", @@ -34,7 +41,7 @@ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a dane logowania i konfiguracj\u0119 Hive.", + "description": "Wprowad\u017a dane logowania Hive.", "title": "Login Hive" } } diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json index e9c91d9df20..8bcf5d96a86 100644 --- a/homeassistant/components/homeassistant/translations/ca.json +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arquitectura de la CPU", + "config_dir": "Directori de configuraci\u00f3", "dev": "Desenvolupador", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/et.json b/homeassistant/components/homeassistant/translations/et.json index 529b84120d7..7b9b675ed6f 100644 --- a/homeassistant/components/homeassistant/translations/et.json +++ b/homeassistant/components/homeassistant/translations/et.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Protsessori arhitektuur", + "config_dir": "Konfiguratsiooni kaust", "dev": "Arendus", "docker": "Docker", "hassio": "Haldur", diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index b4da84596bf..7261dfa1f7a 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Processzor architekt\u00fara", + "config_dir": "Konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r", "dev": "Fejleszt\u00e9s", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/lcn/translations/pl.json b/homeassistant/components/lcn/translations/pl.json index 18446607167..2a2415d4138 100644 --- a/homeassistant/components/lcn/translations/pl.json +++ b/homeassistant/components/lcn/translations/pl.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "otrzymano kod blokady", "fingerprint": "otrzymano kod odcisku palca", "send_keys": "otrzymano klucze wysy\u0142ania", "transmitter": "otrzymano kod nadajnika", diff --git a/homeassistant/components/lg_soundbar/translations/pl.json b/homeassistant/components/lg_soundbar/translations/pl.json new file mode 100644 index 00000000000..1b84366cfa4 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/pl.json b/homeassistant/components/life360/translations/pl.json index 30ba7ddc5b6..2b1c7138079 100644 --- a/homeassistant/components/life360/translations/pl.json +++ b/homeassistant/components/life360/translations/pl.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", "invalid_auth": "Niepoprawne uwierzytelnienie", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Konto jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, "description": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url}). Mo\u017cesz to zrobi\u0107 przed dodaniem kont.", - "title": "Informacje o koncie Life360" + "title": "Konfiguracja konta Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Poka\u017c dane drogowe jako stan", + "driving_speed": "Pr\u0119dko\u015b\u0107 jazdy", + "limit_gps_acc": "Ogranicz dok\u0142adno\u015b\u0107 GPS", + "max_gps_accuracy": "Maksymalna dok\u0142adno\u015b\u0107 GPS (w metrach)", + "set_drive_speed": "Ustaw pr\u00f3g dla pr\u0119dko\u015bci jazdy" + }, + "title": "Opcje konta" } } } diff --git a/homeassistant/components/nextdns/translations/ca.json b/homeassistant/components/nextdns/translations/ca.json new file mode 100644 index 00000000000..83a2c018da5 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest perfil NextDNS ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "profiles": { + "data": { + "profile": "Perfil" + } + }, + "user": { + "data": { + "api_key": "Clau API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/de.json b/homeassistant/components/nextdns/translations/de.json new file mode 100644 index 00000000000..58e470e06ff --- /dev/null +++ b/homeassistant/components/nextdns/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses NextDNS-Profil ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/el.json b/homeassistant/components/nextdns/translations/el.json new file mode 100644 index 00000000000..2d5003a1d7a --- /dev/null +++ b/homeassistant/components/nextdns/translations/el.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb NextDNS \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "profiles": { + "data": { + "profile": "\u03a0\u03c1\u03bf\u03c6\u03af\u03bb" + } + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/et.json b/homeassistant/components/nextdns/translations/et.json new file mode 100644 index 00000000000..9b4d56f51d4 --- /dev/null +++ b/homeassistant/components/nextdns/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "NextDNS-profiil on juba seadistatud." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Vigane API v\u00f5ti", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "profiles": { + "data": { + "profile": "Profiil" + } + }, + "user": { + "data": { + "api_key": "API v\u00f5ti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/hu.json b/homeassistant/components/nextdns/translations/hu.json new file mode 100644 index 00000000000..33b5d858e8a --- /dev/null +++ b/homeassistant/components/nextdns/translations/hu.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ez a NextDNS profil m\u00e1r be van \u00e1ll\u00edtva." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API kulcs" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ja.json b/homeassistant/components/nextdns/translations/ja.json new file mode 100644 index 00000000000..7c1f0614fc1 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ja.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u3053\u306eNextDNS\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "profiles": { + "data": { + "profile": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb" + } + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt-BR.json b/homeassistant/components/nextdns/translations/pt-BR.json new file mode 100644 index 00000000000..2982a6daad7 --- /dev/null +++ b/homeassistant/components/nextdns/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Este perfil NextDNS j\u00e1 est\u00e1 configurado." + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "profiles": { + "data": { + "profile": "Perfil" + } + }, + "user": { + "data": { + "api_key": "Chave de API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/zh-Hant.json b/homeassistant/components/nextdns/translations/zh-Hant.json new file mode 100644 index 00000000000..19d2f137a39 --- /dev/null +++ b/homeassistant/components/nextdns/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "NextDNS \u500b\u4eba\u8a2d\u5b9a\u5df2\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "profiles": { + "data": { + "profile": "\u500b\u4eba\u8a2d\u5b9a" + } + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pl.json b/homeassistant/components/nina/translations/pl.json index 631127679c1..60c97a1c1c0 100644 --- a/homeassistant/components/nina/translations/pl.json +++ b/homeassistant/components/nina/translations/pl.json @@ -23,5 +23,27 @@ "title": "Wybierz miasto/powiat" } } + }, + "options": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_selection": "Prosz\u0119 wybra\u0107 przynajmniej jedno miasto lub powiat", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Miasto/powiat (A-D)", + "_e_to_h": "Miasto/powiat (E-H)", + "_i_to_l": "Miasto/powiat (I-L)", + "_m_to_q": "Miasto/powiat (M-Q)", + "_r_to_u": "Miasto/powiat (R-U)", + "_v_to_z": "Miasto/powiat (V-Z)", + "corona_filter": "Usu\u0144 ostrze\u017cenia o koronawirusie", + "slots": "Maksymalna liczba ostrze\u017ce\u0144 na miasto/powiat" + }, + "title": "Opcje" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/pl.json b/homeassistant/components/qnap_qsw/translations/pl.json index e90d527a7a7..f33acb2c0d6 100644 --- a/homeassistant/components/qnap_qsw/translations/pl.json +++ b/homeassistant/components/qnap_qsw/translations/pl.json @@ -9,6 +9,12 @@ "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { + "discovered_connection": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/simplepush/translations/pl.json b/homeassistant/components/simplepush/translations/pl.json new file mode 100644 index 00000000000..fe19feb39a1 --- /dev/null +++ b/homeassistant/components/simplepush/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "device_key": "Klucz Twojego urz\u0105dzenia", + "event": "Wydarzenie dla wydarze\u0144.", + "name": "Nazwa", + "password": "Has\u0142o szyfrowania u\u017cywane przez Twoje urz\u0105dzenie", + "salt": "Salt u\u017cywane przez Twoje urz\u0105dzenie." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pl.json b/homeassistant/components/soundtouch/translations/pl.json new file mode 100644 index 00000000000..10a760e1acf --- /dev/null +++ b/homeassistant/components/soundtouch/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + }, + "zeroconf_confirm": { + "description": "Zamierzasz doda\u0107 urz\u0105dzenie SoundTouch o nazwie `{name}` do Home Assistanta.", + "title": "Potwierd\u017a dodanie urz\u0105dzenia Bose SoundTouch" + } + } + } +} \ No newline at end of file From 110d9232cd527b471f883e67bfc8e3db0163775c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 5 Jul 2022 08:05:53 +0200 Subject: [PATCH 122/820] Remove melcloud from mypy ignore list (#74410) --- homeassistant/components/melcloud/__init__.py | 4 +-- homeassistant/components/melcloud/climate.py | 33 +++++++++++-------- mypy.ini | 6 ---- script/hassfest/mypy_config.py | 2 -- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 6342c2bc3be..1bb65a943f8 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -142,7 +142,7 @@ class MelCloudDevice: ) -async def mel_devices_setup(hass, token) -> list[MelCloudDevice]: +async def mel_devices_setup(hass, token) -> dict[str, list[MelCloudDevice]]: """Query connected devices from MELCloud.""" session = async_get_clientsession(hass) try: @@ -156,7 +156,7 @@ async def mel_devices_setup(hass, token) -> list[MelCloudDevice]: except (asyncio.TimeoutError, ClientConnectionError) as ex: raise ConfigEntryNotReady() from ex - wrapped_devices = {} + wrapped_devices: dict[str, list[MelCloudDevice]] = {} for device_type, devices in all_devices.items(): wrapped_devices[device_type] = [MelCloudDevice(device) for device in devices] return wrapped_devices diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index b16221923de..a0ffe3a68bb 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -66,16 +66,19 @@ async def async_setup_entry( ) -> None: """Set up MelCloud device climate based on config_entry.""" mel_devices = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities: list[AtaDeviceClimate | AtwDeviceZoneClimate] = [ + AtaDeviceClimate(mel_device, mel_device.device) + for mel_device in mel_devices[DEVICE_TYPE_ATA] + ] + entities.extend( [ - AtaDeviceClimate(mel_device, mel_device.device) - for mel_device in mel_devices[DEVICE_TYPE_ATA] - ] - + [ AtwDeviceZoneClimate(mel_device, mel_device.device, zone) for mel_device in mel_devices[DEVICE_TYPE_ATW] for zone in mel_device.device.zones - ], + ] + ) + async_add_entities( + entities, True, ) @@ -157,14 +160,16 @@ class AtaDeviceClimate(MelCloudClimate): return attr @property - def hvac_mode(self) -> HVACMode: + def hvac_mode(self) -> HVACMode | None: """Return hvac operation ie. heat, cool mode.""" mode = self._device.operation_mode if not self._device.power or mode is None: return HVACMode.OFF return ATA_HVAC_MODE_LOOKUP.get(mode) - def _apply_set_hvac_mode(self, hvac_mode: str, set_dict: dict[str, Any]) -> None: + def _apply_set_hvac_mode( + self, hvac_mode: HVACMode, set_dict: dict[str, Any] + ) -> None: """Apply hvac mode changes to a dict used to call _device.set.""" if hvac_mode == HVACMode.OFF: set_dict["power"] = False @@ -180,7 +185,7 @@ class AtaDeviceClimate(MelCloudClimate): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - set_dict = {} + set_dict: dict[str, Any] = {} self._apply_set_hvac_mode(hvac_mode, set_dict) await self._device.set(set_dict) @@ -188,7 +193,9 @@ class AtaDeviceClimate(MelCloudClimate): def hvac_modes(self) -> list[HVACMode]: """Return the list of available hvac operation modes.""" return [HVACMode.OFF] + [ - ATA_HVAC_MODE_LOOKUP.get(mode) for mode in self._device.operation_modes + ATA_HVAC_MODE_LOOKUP[mode] + for mode in self._device.operation_modes + if mode in ATA_HVAC_MODE_LOOKUP ] @property @@ -201,9 +208,9 @@ class AtaDeviceClimate(MelCloudClimate): """Return the temperature we try to reach.""" return self._device.target_temperature - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - set_dict = {} + set_dict: dict[str, Any] = {} if ATTR_HVAC_MODE in kwargs: self._apply_set_hvac_mode( kwargs.get(ATTR_HVAC_MODE, self.hvac_mode), set_dict @@ -255,7 +262,7 @@ class AtaDeviceClimate(MelCloudClimate): await self.async_set_vane_vertical(swing_mode) @property - def swing_modes(self) -> str | None: + def swing_modes(self) -> list[str] | None: """Return a list of available vertical vane positions and modes.""" return self._device.vane_vertical_positions diff --git a/mypy.ini b/mypy.ini index 30bc97f545a..91dc7f9e71f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2761,12 +2761,6 @@ ignore_errors = true [mypy-homeassistant.components.lyric.sensor] ignore_errors = true -[mypy-homeassistant.components.melcloud] -ignore_errors = true - -[mypy-homeassistant.components.melcloud.climate] -ignore_errors = true - [mypy-homeassistant.components.meteo_france.sensor] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 939290ca9ab..d987a76d41d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -62,8 +62,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lyric.climate", "homeassistant.components.lyric.config_flow", "homeassistant.components.lyric.sensor", - "homeassistant.components.melcloud", - "homeassistant.components.melcloud.climate", "homeassistant.components.meteo_france.sensor", "homeassistant.components.meteo_france.weather", "homeassistant.components.minecraft_server", From b09aaba421d6d6178d582bef9ea363017e55639d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 5 Jul 2022 10:16:38 +0300 Subject: [PATCH 123/820] Add type hints and code cleanup for mikrotik (#74296) * Add type hints and code cleanup for mikrotik * update test and increase coverage * move setup_mikrotik_entry to __init__.py --- .coveragerc | 1 - homeassistant/components/mikrotik/__init__.py | 33 +-- .../components/mikrotik/config_flow.py | 17 +- homeassistant/components/mikrotik/const.py | 48 +++-- .../components/mikrotik/device_tracker.py | 50 +++-- homeassistant/components/mikrotik/hub.py | 203 ++++++++---------- tests/components/mikrotik/__init__.py | 40 ++++ .../mikrotik/test_device_tracker.py | 122 ++++++++--- tests/components/mikrotik/test_hub.py | 120 ----------- tests/components/mikrotik/test_init.py | 1 - 10 files changed, 302 insertions(+), 333 deletions(-) delete mode 100644 tests/components/mikrotik/test_hub.py diff --git a/.coveragerc b/.coveragerc index 21534333583..97f26d8adc5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -717,7 +717,6 @@ omit = homeassistant/components/microsoft/tts.py homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/hub.py - homeassistant/components/mikrotik/device_tracker.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py homeassistant/components/mill/sensor.py diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 856495dc0f2..f72c79c1559 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,35 +1,41 @@ """The Mikrotik component.""" from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ATTR_MANUFACTURER, DOMAIN, PLATFORMS -from .hub import MikrotikDataUpdateCoordinator +from .errors import CannotConnect, LoginError +from .hub import MikrotikDataUpdateCoordinator, get_api CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Mikrotik component.""" - - hub = MikrotikDataUpdateCoordinator(hass, config_entry) - if not await hub.async_setup(): + try: + api = await hass.async_add_executor_job(get_api, dict(config_entry.data)) + except CannotConnect as api_error: + raise ConfigEntryNotReady from api_error + except LoginError: return False - await hub.async_config_entry_first_refresh() + coordinator = MikrotikDataUpdateCoordinator(hass, config_entry, api) + await hass.async_add_executor_job(coordinator.api.get_hub_details) + await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(DOMAIN, hub.serial_num)}, + connections={(DOMAIN, coordinator.serial_num)}, manufacturer=ATTR_MANUFACTURER, - model=hub.model, - name=hub.hostname, - sw_version=hub.firmware, + model=coordinator.model, + name=coordinator.hostname, + sw_version=coordinator.firmware, ) return True @@ -37,10 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( + if unload_ok := await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS - ) - - hass.data[DOMAIN].pop(config_entry.entry_id) + ): + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index 36b65b6f2ba..d506c2c75e4 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Mikrotik.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -13,6 +15,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_ARP_PING, @@ -40,7 +43,9 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return MikrotikOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -52,7 +57,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): break try: - await self.hass.async_add_executor_job(get_api, self.hass, user_input) + await self.hass.async_add_executor_job(get_api, user_input) except CannotConnect: errors["base"] = "cannot_connect" except LoginError: @@ -86,11 +91,15 @@ class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Mikrotik options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Mikrotik options.""" return await self.async_step_device_tracker() - async def async_step_device_tracker(self, user_input=None): + async def async_step_device_tracker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the device tracker options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index b328c10a602..2942e6981fa 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -1,33 +1,35 @@ """Constants used in the Mikrotik components.""" +from typing import Final + from homeassistant.const import Platform -DOMAIN = "mikrotik" -DEFAULT_NAME = "Mikrotik" -DEFAULT_API_PORT = 8728 -DEFAULT_DETECTION_TIME = 300 +DOMAIN: Final = "mikrotik" +DEFAULT_NAME: Final = "Mikrotik" +DEFAULT_API_PORT: Final = 8728 +DEFAULT_DETECTION_TIME: Final = 300 -ATTR_MANUFACTURER = "Mikrotik" -ATTR_SERIAL_NUMBER = "serial-number" -ATTR_FIRMWARE = "current-firmware" -ATTR_MODEL = "model" +ATTR_MANUFACTURER: Final = "Mikrotik" +ATTR_SERIAL_NUMBER: Final = "serial-number" +ATTR_FIRMWARE: Final = "current-firmware" +ATTR_MODEL: Final = "model" -CONF_ARP_PING = "arp_ping" -CONF_FORCE_DHCP = "force_dhcp" -CONF_DETECTION_TIME = "detection_time" +CONF_ARP_PING: Final = "arp_ping" +CONF_FORCE_DHCP: Final = "force_dhcp" +CONF_DETECTION_TIME: Final = "detection_time" -NAME = "name" -INFO = "info" -IDENTITY = "identity" -ARP = "arp" +NAME: Final = "name" +INFO: Final = "info" +IDENTITY: Final = "identity" +ARP: Final = "arp" -CAPSMAN = "capsman" -DHCP = "dhcp" -WIRELESS = "wireless" -IS_WIRELESS = "is_wireless" -IS_CAPSMAN = "is_capsman" +CAPSMAN: Final = "capsman" +DHCP: Final = "dhcp" +WIRELESS: Final = "wireless" +IS_WIRELESS: Final = "is_wireless" +IS_CAPSMAN: Final = "is_capsman" -MIKROTIK_SERVICES = { +MIKROTIK_SERVICES: Final = { ARP: "/ip/arp/getall", CAPSMAN: "/caps-man/registration-table/getall", DHCP: "/ip/dhcp-server/lease/getall", @@ -38,9 +40,9 @@ MIKROTIK_SERVICES = { IS_CAPSMAN: "/caps-man/interface/print", } -PLATFORMS = [Platform.DEVICE_TRACKER] +PLATFORMS: Final = [Platform.DEVICE_TRACKER] -ATTR_DEVICE_TRACKER = [ +ATTR_DEVICE_TRACKER: Final = [ "comment", "mac-address", "ssid", diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 9389d3bea5c..4e002cdbcfe 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -1,6 +1,8 @@ """Support for Mikrotik routers as device tracker.""" from __future__ import annotations +from typing import Any + from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER, @@ -14,7 +16,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.dt as dt_util from .const import DOMAIN -from .hub import MikrotikDataUpdateCoordinator +from .hub import Device, MikrotikDataUpdateCoordinator # These are normalized to ATTR_IP and ATTR_MAC to conform # to device_tracker @@ -27,7 +29,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up device tracker for Mikrotik component.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + coordinator: MikrotikDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] tracked: dict[str, MikrotikDataUpdateCoordinatorTracker] = {} @@ -42,47 +46,53 @@ async def async_setup_entry( ): if ( - entity.unique_id in hub.api.devices - or entity.unique_id not in hub.api.all_devices + entity.unique_id in coordinator.api.devices + or entity.unique_id not in coordinator.api.all_devices ): continue - hub.api.restore_device(entity.unique_id) + coordinator.api.restore_device(entity.unique_id) @callback - def update_hub(): + def update_hub() -> None: """Update the status of the device.""" - update_items(hub, async_add_entities, tracked) + update_items(coordinator, async_add_entities, tracked) - config_entry.async_on_unload(hub.async_add_listener(update_hub)) + config_entry.async_on_unload(coordinator.async_add_listener(update_hub)) update_hub() @callback -def update_items(hub, async_add_entities, tracked): +def update_items( + coordinator: MikrotikDataUpdateCoordinator, + async_add_entities: AddEntitiesCallback, + tracked: dict[str, MikrotikDataUpdateCoordinatorTracker], +): """Update tracked device state from the hub.""" - new_tracked = [] - for mac, device in hub.api.devices.items(): + new_tracked: list[MikrotikDataUpdateCoordinatorTracker] = [] + for mac, device in coordinator.api.devices.items(): if mac not in tracked: - tracked[mac] = MikrotikDataUpdateCoordinatorTracker(device, hub) + tracked[mac] = MikrotikDataUpdateCoordinatorTracker(device, coordinator) new_tracked.append(tracked[mac]) if new_tracked: async_add_entities(new_tracked) -class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): +class MikrotikDataUpdateCoordinatorTracker( + CoordinatorEntity[MikrotikDataUpdateCoordinator], ScannerEntity +): """Representation of network device.""" - coordinator: MikrotikDataUpdateCoordinator - - def __init__(self, device, hub): + def __init__( + self, device: Device, coordinator: MikrotikDataUpdateCoordinator + ) -> None: """Initialize the tracked device.""" - super().__init__(hub) + super().__init__(coordinator) self.device = device @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the client is connected to the network.""" if ( self.device.last_seen @@ -93,7 +103,7 @@ class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): return False @property - def source_type(self): + def source_type(self) -> str: """Return the source type of the client.""" return SOURCE_TYPE_ROUTER @@ -124,7 +134,7 @@ class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): return self.device.mac @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" if self.is_connected: return {k: v for k, v in self.device.attrs.items() if k not in FILTER_ATTRS} diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 7f2314bd057..9219159ca74 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -1,14 +1,18 @@ """The Mikrotik router class.""" -from datetime import timedelta +from __future__ import annotations + +from datetime import datetime, timedelta import logging import socket import ssl +from typing import Any import librouteros from librouteros.login import plain as login_plain, token as login_token +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -42,36 +46,36 @@ _LOGGER = logging.getLogger(__name__) class Device: """Represents a network device.""" - def __init__(self, mac, params): + def __init__(self, mac: str, params: dict[str, Any]) -> None: """Initialize the network device.""" self._mac = mac self._params = params - self._last_seen = None - self._attrs = {} - self._wireless_params = None + self._last_seen: datetime | None = None + self._attrs: dict[str, Any] = {} + self._wireless_params: dict[str, Any] = {} @property - def name(self): + def name(self) -> str: """Return device name.""" return self._params.get("host-name", self.mac) @property - def ip_address(self): + def ip_address(self) -> str: """Return device primary ip address.""" - return self._params.get("address") + return self._params["address"] @property - def mac(self): + def mac(self) -> str: """Return device mac.""" return self._mac @property - def last_seen(self): + def last_seen(self) -> datetime | None: """Return device last seen.""" return self._last_seen @property - def attrs(self): + def attrs(self) -> dict[str, Any]: """Return device attributes.""" attr_data = self._wireless_params if self._wireless_params else self._params for attr in ATTR_DEVICE_TRACKER: @@ -80,7 +84,12 @@ class Device: self._attrs["ip_address"] = self._params.get("active-address") return self._attrs - def update(self, wireless_params=None, params=None, active=False): + def update( + self, + wireless_params: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + active: bool = False, + ) -> None: """Update Device params.""" if wireless_params: self._wireless_params = wireless_params @@ -93,27 +102,26 @@ class Device: class MikrotikData: """Handle all communication with the Mikrotik API.""" - def __init__(self, hass, config_entry, api): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: librouteros.Api + ) -> None: """Initialize the Mikrotik Client.""" self.hass = hass self.config_entry = config_entry self.api = api - self._host = self.config_entry.data[CONF_HOST] - self.all_devices = {} - self.devices = {} - self.available = True - self.support_capsman = False - self.support_wireless = False - self.hostname = None - self.model = None - self.firmware = None - self.serial_number = None + self._host: str = self.config_entry.data[CONF_HOST] + self.all_devices: dict[str, dict[str, Any]] = {} + self.devices: dict[str, Device] = {} + self.support_capsman: bool = False + self.support_wireless: bool = False + self.hostname: str = "" + self.model: str = "" + self.firmware: str = "" + self.serial_number: str = "" @staticmethod - def load_mac(devices=None): + def load_mac(devices: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: """Load dictionary using MAC address as key.""" - if not devices: - return None mac_devices = {} for device in devices: if "mac-address" in device: @@ -122,26 +130,23 @@ class MikrotikData: return mac_devices @property - def arp_enabled(self): + def arp_enabled(self) -> bool: """Return arp_ping option setting.""" - return self.config_entry.options[CONF_ARP_PING] + return self.config_entry.options.get(CONF_ARP_PING, False) @property - def force_dhcp(self): + def force_dhcp(self) -> bool: """Return force_dhcp option setting.""" - return self.config_entry.options[CONF_FORCE_DHCP] + return self.config_entry.options.get(CONF_FORCE_DHCP, False) - def get_info(self, param): + def get_info(self, param: str) -> str: """Return device model name.""" cmd = IDENTITY if param == NAME else INFO - data = self.command(MIKROTIK_SERVICES[cmd]) - return ( - data[0].get(param) # pylint: disable=unsubscriptable-object - if data - else None - ) + if data := self.command(MIKROTIK_SERVICES[cmd]): + return str(data[0].get(param)) + return "" - def get_hub_details(self): + def get_hub_details(self) -> None: """Get Hub info.""" self.hostname = self.get_info(NAME) self.model = self.get_info(ATTR_MODEL) @@ -150,24 +155,17 @@ class MikrotikData: self.support_capsman = bool(self.command(MIKROTIK_SERVICES[IS_CAPSMAN])) self.support_wireless = bool(self.command(MIKROTIK_SERVICES[IS_WIRELESS])) - def connect_to_hub(self): - """Connect to hub.""" - try: - self.api = get_api(self.hass, self.config_entry.data) - return True - except (LoginError, CannotConnect): - return False - - def get_list_from_interface(self, interface): + def get_list_from_interface(self, interface: str) -> dict[str, dict[str, Any]]: """Get devices from interface.""" - result = self.command(MIKROTIK_SERVICES[interface]) - return self.load_mac(result) if result else {} + if result := self.command(MIKROTIK_SERVICES[interface]): + return self.load_mac(result) + return {} - def restore_device(self, mac): + def restore_device(self, mac: str) -> None: """Restore a missing device after restart.""" self.devices[mac] = Device(mac, self.all_devices[mac]) - def update_devices(self): + def update_devices(self) -> None: """Get list of devices with latest status.""" arp_devices = {} device_list = {} @@ -192,7 +190,7 @@ class MikrotikData: # get new hub firmware version if updated self.firmware = self.get_info(ATTR_FIRMWARE) - except (CannotConnect, socket.timeout, OSError) as err: + except (CannotConnect, LoginError) as err: raise UpdateFailed from err if not device_list: @@ -218,11 +216,12 @@ class MikrotikData: active = True if self.arp_enabled and mac in arp_devices: active = self.do_arp_ping( - params.get("active-address"), arp_devices[mac].get("interface") + str(params.get("active-address")), + str(arp_devices[mac].get("interface")), ) self.devices[mac].update(active=active) - def do_arp_ping(self, ip_address, interface): + def do_arp_ping(self, ip_address: str, interface: str) -> bool: """Attempt to arp ping MAC address via interface.""" _LOGGER.debug("pinging - %s", ip_address) params = { @@ -234,9 +233,9 @@ class MikrotikData: } cmd = "/ping" data = self.command(cmd, params) - if data is not None: + if data: status = 0 - for result in data: # pylint: disable=not-an-iterable + for result in data: if "status" in result: status += 1 if status == len(data): @@ -246,22 +245,25 @@ class MikrotikData: return False return True - def command(self, cmd, params=None): + def command( + self, cmd: str, params: dict[str, Any] | None = None + ) -> list[dict[str, Any]]: """Retrieve data from Mikrotik API.""" try: _LOGGER.info("Running command %s", cmd) if params: - response = list(self.api(cmd=cmd, **params)) - else: - response = list(self.api(cmd=cmd)) + return list(self.api(cmd=cmd, **params)) + return list(self.api(cmd=cmd)) except ( librouteros.exceptions.ConnectionClosed, OSError, socket.timeout, ) as api_error: _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) - if not self.connect_to_hub(): - raise CannotConnect from api_error + # try to reconnect + self.api = get_api(dict(self.config_entry.data)) + # we still have to raise CannotConnect to fail the update. + raise CannotConnect from api_error except librouteros.exceptions.ProtocolError as api_error: _LOGGER.warning( "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", @@ -269,106 +271,71 @@ class MikrotikData: cmd, api_error, ) - return None - - return response if response else None + return [] class MikrotikDataUpdateCoordinator(DataUpdateCoordinator): """Mikrotik Hub Object.""" - def __init__(self, hass, config_entry): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: librouteros.Api + ) -> None: """Initialize the Mikrotik Client.""" self.hass = hass - self.config_entry = config_entry - self._mk_data = None + self.config_entry: ConfigEntry = config_entry + self._mk_data = MikrotikData(self.hass, self.config_entry, api) super().__init__( self.hass, _LOGGER, name=f"{DOMAIN} - {self.host}", - update_method=self.async_update, update_interval=timedelta(seconds=10), ) @property - def host(self): + def host(self) -> str: """Return the host of this hub.""" return self.config_entry.data[CONF_HOST] @property - def hostname(self): + def hostname(self) -> str: """Return the hostname of the hub.""" return self._mk_data.hostname @property - def model(self): + def model(self) -> str: """Return the model of the hub.""" return self._mk_data.model @property - def firmware(self): + def firmware(self) -> str: """Return the firmware of the hub.""" return self._mk_data.firmware @property - def serial_num(self): + def serial_num(self) -> str: """Return the serial number of the hub.""" return self._mk_data.serial_number @property - def available(self): - """Return if the hub is connected.""" - return self._mk_data.available - - @property - def option_detection_time(self): + def option_detection_time(self) -> timedelta: """Config entry option defining number of seconds from last seen to away.""" - return timedelta(seconds=self.config_entry.options[CONF_DETECTION_TIME]) + return timedelta( + seconds=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + ) @property - def api(self): + def api(self) -> MikrotikData: """Represent Mikrotik data object.""" return self._mk_data - async def async_add_options(self): - """Populate default options for Mikrotik.""" - if not self.config_entry.options: - data = dict(self.config_entry.data) - options = { - CONF_ARP_PING: data.pop(CONF_ARP_PING, False), - CONF_FORCE_DHCP: data.pop(CONF_FORCE_DHCP, False), - CONF_DETECTION_TIME: data.pop( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ), - } - - self.hass.config_entries.async_update_entry( - self.config_entry, data=data, options=options - ) - - async def async_update(self): + async def _async_update_data(self) -> None: """Update Mikrotik devices information.""" await self.hass.async_add_executor_job(self._mk_data.update_devices) - async def async_setup(self): - """Set up the Mikrotik hub.""" - try: - api = await self.hass.async_add_executor_job( - get_api, self.hass, self.config_entry.data - ) - except CannotConnect as api_error: - raise ConfigEntryNotReady from api_error - except LoginError: - return False - self._mk_data = MikrotikData(self.hass, self.config_entry, api) - await self.async_add_options() - await self.hass.async_add_executor_job(self._mk_data.get_hub_details) - - return True - - -def get_api(hass, entry): +def get_api(entry: dict[str, Any]) -> librouteros.Api: """Connect to Mikrotik hub.""" _LOGGER.debug("Connecting to Mikrotik hub [%s]", entry[CONF_HOST]) diff --git a/tests/components/mikrotik/__init__.py b/tests/components/mikrotik/__init__.py index 6f67eea1a0a..cebbd982350 100644 --- a/tests/components/mikrotik/__init__.py +++ b/tests/components/mikrotik/__init__.py @@ -1,4 +1,7 @@ """Tests for the Mikrotik component.""" +from unittest.mock import patch + +from homeassistant.components import mikrotik from homeassistant.components.mikrotik.const import ( CONF_ARP_PING, CONF_DETECTION_TIME, @@ -14,6 +17,8 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) +from tests.common import MockConfigEntry + MOCK_DATA = { CONF_NAME: "Mikrotik", CONF_HOST: "0.0.0.0", @@ -130,3 +135,38 @@ ARP_DATA = [ "disabled": False, }, ] + + +async def setup_mikrotik_entry(hass, **kwargs): + """Set up Mikrotik integration successfully.""" + support_wireless = kwargs.get("support_wireless", True) + dhcp_data = kwargs.get("dhcp_data", DHCP_DATA) + wireless_data = kwargs.get("wireless_data", WIRELESS_DATA) + + def mock_command(self, cmd, params=None): + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: + return support_wireless + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: + return dhcp_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: + return wireless_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.ARP]: + return ARP_DATA + return {} + + config_entry = MockConfigEntry( + domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + if "force_dhcp" in kwargs: + config_entry.options = {**config_entry.options, "force_dhcp": True} + + if "arp_ping" in kwargs: + config_entry.options = {**config_entry.options, "arp_ping": True} + + with patch("librouteros.connect"), patch.object( + mikrotik.hub.MikrotikData, "command", new=mock_command + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index fbbb016d09f..e3efe6bd39d 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -1,13 +1,14 @@ """The tests for the Mikrotik device tracker platform.""" from datetime import timedelta +from freezegun import freeze_time import pytest from homeassistant.components import mikrotik import homeassistant.components.device_tracker as device_tracker +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util.dt import utcnow from . import ( DEVICE_2_WIRELESS, @@ -17,12 +18,10 @@ from . import ( MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA, + setup_mikrotik_entry, ) -from .test_hub import setup_mikrotik_entry -from tests.common import MockConfigEntry, patch - -DEFAULT_DETECTION_TIME = timedelta(seconds=300) +from tests.common import MockConfigEntry, async_fire_time_changed, patch @pytest.fixture @@ -56,24 +55,11 @@ def mock_command(self, cmd, params=None): return {} -async def test_platform_manually_configured(hass): - """Test that nothing happens when configuring mikrotik through device tracker platform.""" - assert ( - await async_setup_component( - hass, - device_tracker.DOMAIN, - {device_tracker.DOMAIN: {"platform": "mikrotik"}}, - ) - is False - ) - assert mikrotik.DOMAIN not in hass.data - - async def test_device_trackers(hass, mock_device_registry_devices): """Test device_trackers created by mikrotik.""" # test devices are added from wireless list only - hub = await setup_mikrotik_entry(hass) + await setup_mikrotik_entry(hass) device_1 = hass.states.get("device_tracker.device_1") assert device_1 is not None @@ -90,7 +76,7 @@ async def test_device_trackers(hass, mock_device_registry_devices): # test device_2 is added after connecting to wireless network WIRELESS_DATA.append(DEVICE_2_WIRELESS) - await hub.async_refresh() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") @@ -104,26 +90,72 @@ async def test_device_trackers(hass, mock_device_registry_devices): # test state remains home if last_seen consider_home_interval del WIRELESS_DATA[1] # device 2 is removed from wireless list - hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( - minutes=4 - ) - await hub.async_update() - await hass.async_block_till_done() + with freeze_time(utcnow() + timedelta(minutes=4)): + async_fire_time_changed(hass, utcnow() + timedelta(minutes=4)) + await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") - assert device_2.state != "not_home" + assert device_2.state == "home" # test state changes to away if last_seen > consider_home_interval - hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( - minutes=5 - ) - await hub.async_refresh() - await hass.async_block_till_done() + with freeze_time(utcnow() + timedelta(minutes=6)): + async_fire_time_changed(hass, utcnow() + timedelta(minutes=6)) + await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") assert device_2.state == "not_home" +async def test_force_dhcp(hass, mock_device_registry_devices): + """Test updating hub that supports wireless with forced dhcp method.""" + + # hub supports wireless by default, force_dhcp is enabled to override + await setup_mikrotik_entry(hass, force_dhcp=False) + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 + assert device_1.state == "home" + # device_2 is not on the wireless list but it is still added from DHCP + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "home" + + +async def test_hub_not_support_wireless(hass, mock_device_registry_devices): + """Test device_trackers created when hub doesn't support wireless.""" + + await setup_mikrotik_entry(hass, support_wireless=False) + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 + assert device_1.state == "home" + # device_2 is added from DHCP + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "home" + + +async def test_arp_ping_success(hass, mock_device_registry_devices): + """Test arp ping devices to confirm they are connected.""" + + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=True): + await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + # test wired device_2 show as home if arp ping returns True + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "home" + + +async def test_arp_ping_timeout(hass, mock_device_registry_devices): + """Test arp ping timeout so devices are shown away.""" + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=False): + await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + # test wired device_2 show as not_home if arp ping times out + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "not_home" + + async def test_device_trackers_numerical_name(hass, mock_device_registry_devices): """Test device_trackers created by mikrotik with numerical device name.""" @@ -164,6 +196,13 @@ async def test_restoring_devices(hass): suggested_object_id="device_2", config_entry=config_entry, ) + registry.async_get_or_create( + device_tracker.DOMAIN, + mikrotik.DOMAIN, + "00:00:00:00:00:03", + suggested_object_id="device_3", + config_entry=config_entry, + ) await setup_mikrotik_entry(hass) @@ -174,3 +213,22 @@ async def test_restoring_devices(hass): device_2 = hass.states.get("device_tracker.device_2") assert device_2 is not None assert device_2.state == "not_home" + # device_3 is not on the DHCP list or wireless list + # so it won't be restored. + device_3 = hass.states.get("device_tracker.device_3") + assert device_3 is None + + +async def test_update_failed(hass, mock_device_registry_devices): + """Test failing to connect during update.""" + + await setup_mikrotik_entry(hass) + + with patch.object( + mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect + ): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == STATE_UNAVAILABLE diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py deleted file mode 100644 index 1e056071236..00000000000 --- a/tests/components/mikrotik/test_hub.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Test Mikrotik hub.""" -from unittest.mock import patch - -from homeassistant.components import mikrotik - -from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA - -from tests.common import MockConfigEntry - - -async def setup_mikrotik_entry(hass, **kwargs): - """Set up Mikrotik integration successfully.""" - support_wireless = kwargs.get("support_wireless", True) - dhcp_data = kwargs.get("dhcp_data", DHCP_DATA) - wireless_data = kwargs.get("wireless_data", WIRELESS_DATA) - - def mock_command(self, cmd, params=None): - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: - return support_wireless - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: - return dhcp_data - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: - return wireless_data - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.ARP]: - return ARP_DATA - return {} - - config_entry = MockConfigEntry( - domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS - ) - config_entry.add_to_hass(hass) - - if "force_dhcp" in kwargs: - config_entry.options = {**config_entry.options, "force_dhcp": True} - - if "arp_ping" in kwargs: - config_entry.options = {**config_entry.options, "arp_ping": True} - - with patch("librouteros.connect"), patch.object( - mikrotik.hub.MikrotikData, "command", new=mock_command - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - return hass.data[mikrotik.DOMAIN][config_entry.entry_id] - - -async def test_update_failed(hass): - """Test failing to connect during update.""" - - hub = await setup_mikrotik_entry(hass) - - with patch.object( - mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect - ): - await hub.async_refresh() - - assert not hub.last_update_success - - -async def test_hub_not_support_wireless(hass): - """Test updating hub devices when hub doesn't support wireless interfaces.""" - - # test that the devices are constructed from dhcp data - - hub = await setup_mikrotik_entry(hass, support_wireless=False) - - assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] - assert hub.api.devices["00:00:00:00:00:01"]._wireless_params is None - assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] - assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None - - -async def test_hub_support_wireless(hass): - """Test updating hub devices when hub support wireless interfaces.""" - - # test that the device list is from wireless data list - - hub = await setup_mikrotik_entry(hass) - - assert hub.api.support_wireless is True - assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] - assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] - - # devices not in wireless list will not be added - assert "00:00:00:00:00:02" not in hub.api.devices - - -async def test_force_dhcp(hass): - """Test updating hub devices with forced dhcp method.""" - - # test that the devices are constructed from dhcp data - - hub = await setup_mikrotik_entry(hass, force_dhcp=True) - - assert hub.api.support_wireless is True - assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] - assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] - - # devices not in wireless list are added from dhcp - assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] - assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None - - -async def test_arp_ping(hass): - """Test arp ping devices to confirm they are connected.""" - - # test device show as home if arp ping returns value - with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=True): - hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) - - assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None - assert hub.api.devices["00:00:00:00:00:02"].last_seen is not None - - # test device show as away if arp ping times out - with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=False): - hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) - - assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None - # this device is not wireless so it will show as away - assert hub.api.devices["00:00:00:00:00:02"].last_seen is None diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 5ac408928d8..3d7927174b5 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -39,7 +39,6 @@ async def test_successful_config_entry(hass): await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.LOADED - assert hass.data[DOMAIN][entry.entry_id] async def test_hub_conn_error(hass, mock_api): From 2ee5ac02cf315aff59fb1c86846c4a1b8bd291d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Jul 2022 09:50:36 +0200 Subject: [PATCH 124/820] Bump home-assistant/builder from 2022.06.2 to 2022.07.0 (#74446) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 826af2eb9fe..d49f1e3a9c7 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -159,7 +159,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.06.2 + uses: home-assistant/builder@2022.07.0 with: args: | $BUILD_ARGS \ @@ -225,7 +225,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.06.2 + uses: home-assistant/builder@2022.07.0 with: args: | $BUILD_ARGS \ From 6422040262be42d11f1da4cd74a852f3b609c31a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 02:51:53 -0500 Subject: [PATCH 125/820] Remove asserts from lutron_caseta async_attach_trigger (#74429) --- .../components/lutron_caseta/device_trigger.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index d938ad6e7f2..27227619d45 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -429,9 +430,13 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Attach a trigger.""" device_registry = dr.async_get(hass) - device = device_registry.async_get(config[CONF_DEVICE_ID]) - assert device - assert device.model + if ( + not (device := device_registry.async_get(config[CONF_DEVICE_ID])) + or not device.model + ): + raise HomeAssistantError( + f"Cannot attach trigger {config} because device with id {config[CONF_DEVICE_ID]} is missing or invalid" + ) device_type = _device_model_to_type(device.model) _, serial = list(device.identifiers)[0] schema = DEVICE_TYPE_SCHEMA_MAP[device_type] From f975d30258209ad4d133990d5c96e870e71fcf86 Mon Sep 17 00:00:00 2001 From: Arne Mauer Date: Tue, 5 Jul 2022 10:35:05 +0200 Subject: [PATCH 126/820] Fix multi_match to match with the IKEA airpurifier channel (#74432) Fix multi_match for FilterLifeTime, device_run_time, filter_run_time sensors for ikea starkvind --- homeassistant/components/zha/number.py | 6 +----- homeassistant/components/zha/sensor.py | 16 ++-------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index c3d7f352318..e1268e29190 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -526,11 +526,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati @CONFIG_DIAGNOSTIC_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, + channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} ) class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): """Representation of a ZHA timer duration configuration entity.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 2fe38193ecb..4a4700b3c4c 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -810,13 +810,7 @@ class TimeLeft(Sensor, id_suffix="time_left"): _unit = TIME_MINUTES -@MULTI_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, -) +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): """Sensor that displays device run time (in minutes).""" @@ -826,13 +820,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): _unit = TIME_MINUTES -@MULTI_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, -) +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): """Sensor that displays run time of the current filter (in minutes).""" From b5c55311807de0a1abfc5b9a25a3588e33d0e536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Br=C3=BCckmann?= Date: Tue, 5 Jul 2022 12:25:20 +0200 Subject: [PATCH 127/820] Fix unreachable DenonAVR reporting as available when polling fails (#74344) --- homeassistant/components/denonavr/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 8d3102c441b..85e28c29d7c 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -204,12 +204,14 @@ class DenonDevice(MediaPlayerEntity): ) self._available = False except AvrCommandError as err: + available = False _LOGGER.error( "Command %s failed with error: %s", func.__name__, err, ) except DenonAvrError as err: + available = False _LOGGER.error( "Error %s occurred in method %s for Denon AVR receiver", err, From a1a887ddac810eed524de728edb8743845e8f438 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 5 Jul 2022 12:27:00 +0200 Subject: [PATCH 128/820] Add GeolocationEvent checks to pylint plugin (#74286) --- pylint/plugins/hass_enforce_type_hints.py | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 8e846cf5db0..7996afdc545 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1205,6 +1205,33 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "geo_location": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="GeolocationEvent", + matches=[ + TypeHintMatch( + function_name="source", + return_type="str", + ), + TypeHintMatch( + function_name="distance", + return_type=["float", None], + ), + TypeHintMatch( + function_name="latitude", + return_type=["float", None], + ), + TypeHintMatch( + function_name="longitude", + return_type=["float", None], + ), + ], + ), + ], "light": [ ClassTypeHintMatch( base_class="Entity", From 809f101f5536e229601d7eec7a85e895e1e52216 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Jul 2022 13:41:33 +0200 Subject: [PATCH 129/820] Re-introduce default scan interval in Scrape sensor (#74455) --- homeassistant/components/scrape/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index e15f7c5ba97..88c9b564b29 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -39,6 +40,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=10) + CONF_ATTR = "attribute" CONF_SELECT = "select" CONF_INDEX = "index" From f6cb2833cacf0c197b358dec9e7758b5b3d09917 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 09:25:30 -0500 Subject: [PATCH 130/820] Improve fans in homekit_controller (#74440) --- .../components/homekit_controller/climate.py | 84 +++++++++++------ .../components/homekit_controller/fan.py | 37 +++++++- .../components/homekit_controller/number.py | 71 --------------- .../homekit_controller/fixtures/haa_fan.json | 4 +- .../specific_devices/test_ecobee_501.py | 3 + .../specific_devices/test_haa_fan.py | 8 +- .../homekit_controller/test_climate.py | 30 +++++++ .../components/homekit_controller/test_fan.py | 77 +++++++++++++++- .../homekit_controller/test_number.py | 89 ------------------- 9 files changed, 205 insertions(+), 198 deletions(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 44ecee13875..b76ed1ea6a9 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -25,6 +25,8 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + FAN_AUTO, + FAN_ON, SWING_OFF, SWING_VERTICAL, ClimateEntityFeature, @@ -72,6 +74,7 @@ TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS = { TargetHeaterCoolerStateValues.COOL: HVACMode.COOL, } + # Map of hass operation modes to homekit modes MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} @@ -104,19 +107,65 @@ async def async_setup_entry( conn.add_listener(async_add_service) -class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): - """Representation of a Homekit climate device.""" +class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity): + """The base HomeKit Controller climate entity.""" + + _attr_temperature_unit = TEMP_CELSIUS def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ + CharacteristicsTypes.TEMPERATURE_CURRENT, + CharacteristicsTypes.FAN_STATE_TARGET, + ] + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) + + @property + def fan_modes(self) -> list[str] | None: + """Return the available fan modes.""" + if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET): + return [FAN_ON, FAN_AUTO] + return None + + @property + def fan_mode(self) -> str | None: + """Return the current fan mode.""" + fan_mode = self.service.value(CharacteristicsTypes.FAN_STATE_TARGET) + return FAN_AUTO if fan_mode else FAN_ON + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Turn fan to manual/auto.""" + await self.async_put_characteristics( + {CharacteristicsTypes.FAN_STATE_TARGET: int(fan_mode == FAN_AUTO)} + ) + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + features = 0 + + if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET): + features |= ClimateEntityFeature.FAN_MODE + + return features + + +class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): + """Representation of a Homekit climate device.""" + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity cares about.""" + return super().get_characteristic_types() + [ CharacteristicsTypes.ACTIVE, CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE, CharacteristicsTypes.TARGET_HEATER_COOLER_STATE, CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, CharacteristicsTypes.SWING_MODE, - CharacteristicsTypes.TEMPERATURE_CURRENT, ] async def async_set_temperature(self, **kwargs: Any) -> None: @@ -162,11 +211,6 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): } ) - @property - def current_temperature(self) -> float: - """Return the current temperature.""" - return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) - @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" @@ -321,7 +365,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): @property def supported_features(self) -> int: """Return the list of supported features.""" - features = 0 + features = super().supported_features if self.service.has(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD): features |= ClimateEntityFeature.TARGET_TEMPERATURE @@ -334,22 +378,16 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): return features - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - -class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): +class HomeKitClimateEntity(HomeKitBaseClimateEntity): """Representation of a Homekit climate device.""" def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" - return [ + return super().get_characteristic_types() + [ CharacteristicsTypes.HEATING_COOLING_CURRENT, CharacteristicsTypes.HEATING_COOLING_TARGET, CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, - CharacteristicsTypes.TEMPERATURE_CURRENT, CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_TARGET, CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, @@ -411,11 +449,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): } ) - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) - @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" @@ -558,7 +591,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): @property def supported_features(self) -> int: """Return the list of supported features.""" - features = 0 + features = super().supported_features if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): features |= ClimateEntityFeature.TARGET_TEMPERATURE @@ -573,11 +606,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): return features - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - ENTITY_TYPES = { ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity, diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 80c2f9870c1..159a1d936fa 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -15,6 +15,10 @@ from homeassistant.components.fan import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from . import KNOWN_DEVICES, HomeKitEntity @@ -48,13 +52,32 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): """Return true if device is on.""" return self.service.value(self.on_characteristic) == 1 + @property + def _speed_range(self) -> tuple[int, int]: + """Return the speed range.""" + return (self._min_speed, self._max_speed) + + @property + def _min_speed(self) -> int: + """Return the minimum speed.""" + return ( + round(self.service[CharacteristicsTypes.ROTATION_SPEED].minValue or 0) + 1 + ) + + @property + def _max_speed(self) -> int: + """Return the minimum speed.""" + return round(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100) + @property def percentage(self) -> int: """Return the current speed percentage.""" if not self.is_on: return 0 - return self.service.value(CharacteristicsTypes.ROTATION_SPEED) + return ranged_value_to_percentage( + self._speed_range, self.service.value(CharacteristicsTypes.ROTATION_SPEED) + ) @property def current_direction(self) -> str: @@ -88,7 +111,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): def speed_count(self) -> int: """Speed count for the fan.""" return round( - min(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100, 100) + min(self._max_speed, 100) / max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0) ) @@ -104,7 +127,11 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): return await self.async_turn_off() await self.async_put_characteristics( - {CharacteristicsTypes.ROTATION_SPEED: percentage} + { + CharacteristicsTypes.ROTATION_SPEED: round( + percentage_to_ranged_value(self._speed_range, percentage) + ) + } ) async def async_oscillate(self, oscillating: bool) -> None: @@ -129,7 +156,9 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): percentage is not None and self.supported_features & FanEntityFeature.SET_SPEED ): - characteristics[CharacteristicsTypes.ROTATION_SPEED] = percentage + characteristics[CharacteristicsTypes.ROTATION_SPEED] = round( + percentage_to_ranged_value(self._speed_range, percentage) + ) if characteristics: await self.async_put_characteristics(characteristics) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 07d22c27314..7a6d0a01ab6 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -67,8 +67,6 @@ async def async_setup_entry( if description := NUMBER_ENTITIES.get(char.type): entities.append(HomeKitNumber(conn, info, char, description)) - elif entity_type := NUMBER_ENTITY_CLASSES.get(char.type): - entities.append(entity_type(conn, info, char)) else: return False @@ -130,72 +128,3 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): self._char.type: value, } ) - - -class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity): - """Representation of a Number control for Ecobee Fan Mode request.""" - - def get_characteristic_types(self) -> list[str]: - """Define the homekit characteristics the entity is tracking.""" - return [self._char.type] - - @property - def name(self) -> str: - """Return the name of the device if any.""" - prefix = "" - if name := super().name: - prefix = name - return f"{prefix} Fan Mode" - - @property - def native_min_value(self) -> float: - """Return the minimum value.""" - return self._char.minValue or DEFAULT_MIN_VALUE - - @property - def native_max_value(self) -> float: - """Return the maximum value.""" - return self._char.maxValue or DEFAULT_MAX_VALUE - - @property - def native_step(self) -> float: - """Return the increment/decrement step.""" - return self._char.minStep or DEFAULT_STEP - - @property - def native_value(self) -> float: - """Return the current characteristic value.""" - return self._char.value - - async def async_set_native_value(self, value: float) -> None: - """Set the characteristic to this value.""" - - # Sending the fan mode request sometimes ends up getting ignored by ecobee - # and this might be because it the older value instead of newer, and ecobee - # thinks there is nothing to do. - # So in order to make sure that the request is executed by ecobee, we need - # to send a different value before sending the target value. - # Fan mode value is a value from 0 to 100. We send a value off by 1 first. - - if value > self.min_value: - other_value = value - 1 - else: - other_value = self.min_value + 1 - - if value != other_value: - await self.async_put_characteristics( - { - self._char.type: other_value, - } - ) - - await self.async_put_characteristics( - { - self._char.type: value, - } - ) - - -NUMBER_ENTITY_CLASSES: dict[str, type] = { - CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: HomeKitEcobeeFanModeNumber, -} diff --git a/tests/components/homekit_controller/fixtures/haa_fan.json b/tests/components/homekit_controller/fixtures/haa_fan.json index 14a215d01fe..a144a9501ba 100644 --- a/tests/components/homekit_controller/fixtures/haa_fan.json +++ b/tests/components/homekit_controller/fixtures/haa_fan.json @@ -70,7 +70,7 @@ "perms": ["pr", "pw", "ev"], "ev": true, "format": "bool", - "value": false + "value": true }, { "aid": 1, @@ -83,7 +83,7 @@ "minValue": 0, "maxValue": 3, "minStep": 1, - "value": 3 + "value": 2 } ] }, diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py index 89443008683..ca91607bd09 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py @@ -2,6 +2,7 @@ from homeassistant.components.climate.const import ( + SUPPORT_FAN_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -43,9 +44,11 @@ async def test_ecobee501_setup(hass): SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_HUMIDITY + | SUPPORT_FAN_MODE ), capabilities={ "hvac_modes": ["off", "heat", "cool", "heat_cool"], + "fan_modes": ["on", "auto"], "min_temp": 7.2, "max_temp": 33.3, "min_humidity": 20, diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index 9d5983650d7..39169ea5af9 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -1,6 +1,6 @@ """Make sure that a H.A.A. fan can be setup.""" -from homeassistant.components.fan import SUPPORT_SET_SPEED +from homeassistant.components.fan import ATTR_PERCENTAGE, SUPPORT_SET_SPEED from homeassistant.helpers.entity import EntityCategory from tests.components.homekit_controller.common import ( @@ -18,7 +18,9 @@ async def test_haa_fan_setup(hass): accessories = await setup_accessories_from_file(hass, "haa_fan.json") await setup_test_accessories(hass, accessories) - # FIXME: assert round(state.attributes["percentage_step"], 2) == 33.33 + haa_fan_state = hass.states.get("fan.haa_c718b3") + attributes = haa_fan_state.attributes + assert attributes[ATTR_PERCENTAGE] == 66 await assert_devices_and_entities_created( hass, @@ -55,7 +57,7 @@ async def test_haa_fan_setup(hass): entity_id="fan.haa_c718b3", friendly_name="HAA-C718B3", unique_id="homekit-C718B3-1-8", - state="off", + state="on", supported_features=SUPPORT_SET_SPEED, capabilities={ "preset_modes": None, diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 646804a86d6..c750c428437 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -10,6 +10,7 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.climate.const import ( DOMAIN, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_SWING_MODE, @@ -32,6 +33,9 @@ def create_thermostat_service(accessory): char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT) char.value = 0 + char = service.add_char(CharacteristicsTypes.FAN_STATE_TARGET) + char.value = 0 + char = service.add_char(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD) char.minValue = 15 char.maxValue = 40 @@ -144,6 +148,32 @@ async def test_climate_change_thermostat_state(hass, utcnow): }, ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {"entity_id": "climate.testdevice", "fan_mode": "on"}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.FAN_STATE_TARGET: 0, + }, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {"entity_id": "climate.testdevice", "fan_mode": "auto"}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.FAN_STATE_TARGET: 1, + }, + ) + async def test_climate_check_min_max_values_per_mode(hass, utcnow): """Test that we we get the appropriate min/max values for each mode.""" diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index faaaa2e666f..9d166531562 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -1,4 +1,4 @@ -"""Basic checks for HomeKit motion sensors and contact sensors.""" +"""Basic checks for HomeKit fans.""" from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -41,6 +41,20 @@ def create_fanv2_service(accessory): swing_mode.value = 0 +def create_fanv2_service_non_standard_rotation_range(accessory): + """Define fan v2 with a non-standard rotation range.""" + service = accessory.add_service(ServicesTypes.FAN_V2) + + cur_state = service.add_char(CharacteristicsTypes.ACTIVE) + cur_state.value = 0 + + speed = service.add_char(CharacteristicsTypes.ROTATION_SPEED) + speed.value = 0 + speed.minValue = 0 + speed.maxValue = 3 + speed.minStep = 1 + + def create_fanv2_service_with_min_step(accessory): """Define fan v2 characteristics as per HAP spec.""" service = accessory.add_service(ServicesTypes.FAN_V2) @@ -730,3 +744,64 @@ async def test_v2_oscillate_read(hass, utcnow): ServicesTypes.FAN_V2, {CharacteristicsTypes.SWING_MODE: 1} ) assert state.attributes["oscillating"] is True + + +async def test_v2_set_percentage_non_standard_rotation_range(hass, utcnow): + """Test that we set fan speed with a non-standard rotation range.""" + helper = await setup_test_component( + hass, create_fanv2_service_non_standard_rotation_range + ) + + await helper.async_update(ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: 1}) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 100}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 3, + }, + ) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 66}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 2, + }, + ) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 33}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 1, + }, + ) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 0}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + }, + ) diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index 78bdb394f0c..6b375b60d9b 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -26,26 +26,6 @@ def create_switch_with_spray_level(accessory): return service -def create_switch_with_ecobee_fan_mode(accessory): - """Define battery level characteristics.""" - service = accessory.add_service(ServicesTypes.OUTLET) - - ecobee_fan_mode = service.add_char( - CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED - ) - - ecobee_fan_mode.value = 0 - ecobee_fan_mode.minStep = 1 - ecobee_fan_mode.minValue = 0 - ecobee_fan_mode.maxValue = 100 - ecobee_fan_mode.format = "float" - - cur_state = service.add_char(CharacteristicsTypes.ON) - cur_state.value = True - - return service - - async def test_read_number(hass, utcnow): """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_spray_level) @@ -106,72 +86,3 @@ async def test_write_number(hass, utcnow): ServicesTypes.OUTLET, {CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: 3}, ) - - -async def test_write_ecobee_fan_mode_number(hass, utcnow): - """Test a switch service that has a sensor characteristic is correctly handled.""" - helper = await setup_test_component(hass, create_switch_with_ecobee_fan_mode) - - # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. - fan_mode = Helper( - hass, - "number.testdevice_fan_mode", - helper.pairing, - helper.accessory, - helper.config_entry, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 1}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 1}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 2}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 2}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 99}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 99}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 100}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 100}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 0}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 0}, - ) From b590e51f887d6e552296b35dc1131162638473bb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 5 Jul 2022 18:54:03 +0200 Subject: [PATCH 131/820] Bump deCONZ dependency to v96 (#74460) --- homeassistant/components/deconz/light.py | 6 +----- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 669800e2662..7f3a47d719d 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -113,11 +113,7 @@ async def async_setup_entry( first = True for light_id in group.lights: - if ( - (light := gateway.api.lights.lights.get(light_id)) - and light.ZHATYPE == Light.ZHATYPE - and light.reachable - ): + if (light := gateway.api.lights.lights.get(light_id)) and light.reachable: group.update_color_state(light, update_all_attributes=first) first = False diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 09dcc190a4f..c19d75ec054 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==95"], + "requirements": ["pydeconz==96"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3c6a0ca8d63..52ce487b265 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==95 +pydeconz==96 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18f38d1d230..0198c535090 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==95 +pydeconz==96 # homeassistant.components.dexcom pydexcom==0.2.3 From e7b2d4672c8424679cd06d4d31a65b44b6a7d85a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 12:27:27 -0500 Subject: [PATCH 132/820] Avoid loading mqtt for type checking (#74464) --- homeassistant/helpers/config_entry_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 1190e947eba..3617c0b1f29 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import dhcp, mqtt, onboarding, ssdp, zeroconf +from homeassistant.components import dhcp, onboarding, ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -15,6 +15,9 @@ from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType if TYPE_CHECKING: import asyncio + from homeassistant.components import mqtt + + _R = TypeVar("_R", bound="Awaitable[bool] | bool") DiscoveryFunctionType = Callable[[HomeAssistant], _R] From 87d7c024bf3a8c3d8de2248a263526979074fff1 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 5 Jul 2022 12:43:10 -0500 Subject: [PATCH 133/820] Bump Frontend to 20220705.0 (#74467) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 27ff0a73f20..6b378fe1098 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220630.0"], + "requirements": ["home-assistant-frontend==20220705.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a62ae0b6fd1..e05a9e7e800 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 52ce487b265..e5e6a2a073f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0198c535090..9735a85fec3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 # homeassistant.components.home_connect homeconnect==0.7.1 From e4fd5100c4e7fa9766283a6a1d92ee08016239cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 12:43:38 -0500 Subject: [PATCH 134/820] Bump aiohomekit to 0.7.19 (#74463) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3b3c5e51cf8..a15c576c313 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.18"], + "requirements": ["aiohomekit==0.7.19"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index e5e6a2a073f..e688588c8d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.18 +aiohomekit==0.7.19 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9735a85fec3..06f4e581de0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.18 +aiohomekit==0.7.19 # homeassistant.components.emulated_hue # homeassistant.components.http From 89ab78371f3a2eebf6c9b52afcd371f5984e639e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 5 Jul 2022 20:18:14 +0200 Subject: [PATCH 135/820] Bump Sensibo dependency (#74466) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 18e93d5efa6..5ef8ff6fa4e 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.17"], + "requirements": ["pysensibo==1.0.18"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index e688588c8d7..82a7b2aa99c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1795,7 +1795,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.17 +pysensibo==1.0.18 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06f4e581de0..beda687d9f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1223,7 +1223,7 @@ pyruckus==0.12 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.17 +pysensibo==1.0.18 # homeassistant.components.serial # homeassistant.components.zha From cbe9eda0a8201c988d95a73da0f12a1b57bcf868 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 5 Jul 2022 20:24:18 +0200 Subject: [PATCH 136/820] Remove lyric from mypy ignore list (#74451) --- homeassistant/components/lyric/climate.py | 5 +++-- homeassistant/components/lyric/config_flow.py | 8 ++++++-- homeassistant/components/lyric/sensor.py | 8 +++++--- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index b5c1fb05efa..8353ae15b3c 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import logging from time import localtime, strftime, time +from typing import Any from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation @@ -186,7 +187,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return self.device.indoorTemperature @property - def hvac_action(self) -> HVACAction: + def hvac_action(self) -> HVACAction | None: """Return the current hvac action.""" action = HVAC_ACTIONS.get(self.device.operationStatus.mode, None) if action == HVACAction.OFF and self.hvac_mode != HVACMode.OFF: @@ -265,7 +266,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return device.maxHeatSetpoint return device.maxCoolSetpoint - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if self.hvac_mode == HVACMode.OFF: return diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 12f91cfe206..de6808dd0de 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Honeywell Lyric.""" +from __future__ import annotations + from collections.abc import Mapping import logging from typing import Any @@ -25,13 +27,15 @@ class OAuth2FlowHandler( """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an oauth config entry or update existing entry for reauth.""" existing_entry = await self.async_set_unique_id(DOMAIN) if existing_entry: diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 5b60935d667..d727b24eee4 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -47,9 +47,11 @@ class LyricSensorEntityDescription(SensorEntityDescription): value: Callable[[LyricDevice], StateType | datetime] = round -def get_datetime_from_future_time(time: str) -> datetime: +def get_datetime_from_future_time(time_str: str) -> datetime: """Get datetime from future time provided.""" - time = dt_util.parse_time(time) + time = dt_util.parse_time(time_str) + if time is None: + raise ValueError(f"Unable to parse time {time_str}") now = dt_util.utcnow() if time <= now.time(): now = now + timedelta(days=1) @@ -64,7 +66,7 @@ async def async_setup_entry( entities = [] - def get_setpoint_status(status: str, time: str) -> str: + def get_setpoint_status(status: str, time: str) -> str | None: if status == PRESET_HOLD_UNTIL: return f"Held until {time}" return LYRIC_SETPOINT_STATUS_NAMES.get(status, None) diff --git a/mypy.ini b/mypy.ini index 91dc7f9e71f..52c4dce061f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2752,15 +2752,6 @@ ignore_errors = true [mypy-homeassistant.components.lovelace.websocket] ignore_errors = true -[mypy-homeassistant.components.lyric.climate] -ignore_errors = true - -[mypy-homeassistant.components.lyric.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.lyric.sensor] -ignore_errors = true - [mypy-homeassistant.components.meteo_france.sensor] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d987a76d41d..521c98ea93d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -59,9 +59,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", "homeassistant.components.lovelace.websocket", - "homeassistant.components.lyric.climate", - "homeassistant.components.lyric.config_flow", - "homeassistant.components.lyric.sensor", "homeassistant.components.meteo_france.sensor", "homeassistant.components.meteo_france.weather", "homeassistant.components.minecraft_server", From a7158fee6784939fccaa10639757137dd136f0ae Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 5 Jul 2022 22:24:08 +0200 Subject: [PATCH 137/820] Revert "Migrate aemet to native_*" (#74471) --- homeassistant/components/aemet/__init__.py | 39 +-------- homeassistant/components/aemet/const.py | 26 ++---- homeassistant/components/aemet/sensor.py | 16 ++-- homeassistant/components/aemet/weather.py | 20 ++--- .../aemet/weather_update_coordinator.py | 20 ++--- tests/components/aemet/test_init.py | 84 ------------------- 6 files changed, 37 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 7b86a5559e0..a914a23a0da 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,30 +1,18 @@ """The AEMET OpenData component.""" -from __future__ import annotations - import logging -from typing import Any from aemet_opendata.interface import AEMET from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - Platform, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant from .const import ( CONF_STATION_UPDATES, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, - FORECAST_MODES, PLATFORMS, - RENAMED_FORECAST_SENSOR_KEYS, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -33,8 +21,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AEMET OpenData as config entry.""" - await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) - name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] @@ -74,24 +60,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -@callback -def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: - """Migrate AEMET entity entries. - - - Migrates unique ID from old forecast sensors to the new unique ID - """ - if entry.domain != Platform.SENSOR: - return None - for old_key, new_key in RENAMED_FORECAST_SENSOR_KEYS.items(): - for forecast_mode in FORECAST_MODES: - old_suffix = f"-forecast-{forecast_mode}-{old_key}" - if entry.unique_id.endswith(old_suffix): - new_suffix = f"-forecast-{forecast_mode}-{new_key}" - return { - "new_unique_id": entry.unique_id.replace(old_suffix, new_suffix) - } - - # No migration needed - return None diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 48e7335934f..4be90011f5a 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -18,10 +18,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -163,13 +159,13 @@ CONDITIONS_MAP = { FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -210,7 +206,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRECIPITATION, + key=ATTR_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), @@ -220,13 +216,13 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP, + key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP_LOW, + key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, @@ -242,17 +238,11 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_WIND_SPEED, + key=ATTR_FORECAST_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), ) -RENAMED_FORECAST_SENSOR_KEYS = { - ATTR_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, -} WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_CONDITION, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 8439b166a47..f98e3fff49e 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -45,13 +45,17 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - f"{domain_data[ENTRY_NAME]} {mode} Forecast", - f"{unique_id}-forecast-{mode}", + name_prefix, + unique_id_prefix, weather_coordinator, mode, description, ) for mode in FORECAST_MODES + if ( + (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") + and (unique_id_prefix := f"{unique_id}-forecast-{mode}") + ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -85,14 +89,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id_prefix, + unique_id, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id_prefix}-{description.key}", + unique_id=f"{unique_id}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -109,7 +113,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id_prefix, + unique_id, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -117,7 +121,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id_prefix}-{description.key}", + unique_id=f"{unique_id}-{description.key}", coordinator=weather_coordinator, description=description, ) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index a67726d1f51..d05442b621e 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,12 +1,7 @@ """Support for the AEMET OpenData service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, -) +from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -52,10 +47,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_temperature_unit = TEMP_CELSIUS + _attr_pressure_unit = PRESSURE_HPA + _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -89,12 +83,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def native_pressure(self): + def pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def native_temperature(self): + def temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -104,6 +98,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def native_wind_speed(self): + def wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 4f0bf6ac5ea..c86465ea8f1 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -44,13 +44,13 @@ import async_timeout from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -406,10 +406,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_NATIVE_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_NATIVE_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } @@ -421,13 +421,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return { ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_NATIVE_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_TEMP: self._get_temperature(day, hour), ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index 8dd177a145d..b1f452c1b46 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -2,15 +2,11 @@ from unittest.mock import patch -import pytest import requests_mock from homeassistant.components.aemet.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .util import aemet_requests_mock @@ -46,83 +42,3 @@ async def test_unload_entry(hass): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED - - -@pytest.mark.parametrize( - "old_unique_id,new_unique_id", - [ - # Sensors which should be migrated - ( - "aemet_unique_id-forecast-daily-precipitation", - "aemet_unique_id-forecast-daily-native_precipitation", - ), - ( - "aemet_unique_id-forecast-daily-temperature", - "aemet_unique_id-forecast-daily-native_temperature", - ), - ( - "aemet_unique_id-forecast-daily-templow", - "aemet_unique_id-forecast-daily-native_templow", - ), - ( - "aemet_unique_id-forecast-daily-wind_speed", - "aemet_unique_id-forecast-daily-native_wind_speed", - ), - ( - "aemet_unique_id-forecast-hourly-precipitation", - "aemet_unique_id-forecast-hourly-native_precipitation", - ), - ( - "aemet_unique_id-forecast-hourly-temperature", - "aemet_unique_id-forecast-hourly-native_temperature", - ), - ( - "aemet_unique_id-forecast-hourly-templow", - "aemet_unique_id-forecast-hourly-native_templow", - ), - ( - "aemet_unique_id-forecast-hourly-wind_speed", - "aemet_unique_id-forecast-hourly-native_wind_speed", - ), - # Already migrated - ( - "aemet_unique_id-forecast-daily-native_templow", - "aemet_unique_id-forecast-daily-native_templow", - ), - # No migration needed - ( - "aemet_unique_id-forecast-daily-condition", - "aemet_unique_id-forecast-daily-condition", - ), - ], -) -async def test_migrate_unique_id_sensor( - hass: HomeAssistant, - old_unique_id: str, - new_unique_id: str, -) -> None: - """Test migration of unique_id.""" - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG - ) - config_entry.add_to_hass(hass) - - entity_registry = er.async_get(hass) - entity: er.RegistryEntry = entity_registry.async_get_or_create( - domain=SENSOR_DOMAIN, - platform=DOMAIN, - unique_id=old_unique_id, - config_entry=config_entry, - ) - assert entity.unique_id == old_unique_id - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entity_migrated = entity_registry.async_get(entity.entity_id) - assert entity_migrated - assert entity_migrated.unique_id == new_unique_id From 2e81be77216315c31c38334d912beca55a7197ad Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 6 Jul 2022 00:27:50 +0000 Subject: [PATCH 138/820] [ci skip] Translation update --- .../components/generic/translations/it.json | 2 ++ .../components/nextdns/translations/it.json | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 homeassistant/components/nextdns/translations/it.json diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 1cd63544700..80f8c90ce16 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", + "malformed_url": "URL malformato", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", + "relative_url": "Non sono consentiti URL relativi", "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", diff --git a/homeassistant/components/nextdns/translations/it.json b/homeassistant/components/nextdns/translations/it.json new file mode 100644 index 00000000000..67c9520f2d4 --- /dev/null +++ b/homeassistant/components/nextdns/translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Questo profilo NextDNS \u00e8 gi\u00e0 configurato." + }, + "step": { + "profiles": { + "data": { + "profile": "Profilo" + } + } + } + } +} \ No newline at end of file From f5e3344bfc4616b2b4bf4a71b3e725d876f59875 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 05:10:04 +0200 Subject: [PATCH 139/820] Add NextDNS system health (#74368) Add system_health --- homeassistant/components/nextdns/strings.json | 5 +++ .../components/nextdns/system_health.py | 25 ++++++++++++ .../components/nextdns/test_system_health.py | 40 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 homeassistant/components/nextdns/system_health.py create mode 100644 tests/components/nextdns/test_system_health.py diff --git a/homeassistant/components/nextdns/strings.json b/homeassistant/components/nextdns/strings.json index db3cf88cf39..59319881c02 100644 --- a/homeassistant/components/nextdns/strings.json +++ b/homeassistant/components/nextdns/strings.json @@ -20,5 +20,10 @@ "abort": { "already_configured": "This NextDNS profile is already configured." } + }, + "system_health": { + "info": { + "can_reach_server": "Reach server" + } } } diff --git a/homeassistant/components/nextdns/system_health.py b/homeassistant/components/nextdns/system_health.py new file mode 100644 index 00000000000..0fa31e75f1e --- /dev/null +++ b/homeassistant/components/nextdns/system_health.py @@ -0,0 +1,25 @@ +"""Provide info to system health.""" +from __future__ import annotations + +from typing import Any + +from nextdns.const import API_ENDPOINT + +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_register( # pylint:disable=unused-argument + hass: HomeAssistant, + register: system_health.SystemHealthRegistration, +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: + """Get info for the info page.""" + return { + "can_reach_server": system_health.async_check_can_reach_url(hass, API_ENDPOINT) + } diff --git a/tests/components/nextdns/test_system_health.py b/tests/components/nextdns/test_system_health.py new file mode 100644 index 00000000000..4fc3a1a7e72 --- /dev/null +++ b/tests/components/nextdns/test_system_health.py @@ -0,0 +1,40 @@ +"""Test NextDNS system health.""" +import asyncio + +from aiohttp import ClientError +from nextdns.const import API_ENDPOINT + +from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import get_system_health_info + + +async def test_nextdns_system_health(hass, aioclient_mock): + """Test NextDNS system health.""" + aioclient_mock.get(API_ENDPOINT, text="") + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": "ok"} + + +async def test_nextdns_system_health_fail(hass, aioclient_mock): + """Test NextDNS system health.""" + aioclient_mock.get(API_ENDPOINT, exc=ClientError) + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": {"type": "failed", "error": "unreachable"}} From a2a4361d6edaeaa69db0ad24076359e26a7e1f7f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 05:12:09 +0200 Subject: [PATCH 140/820] Address late comment for NextDNS (#74365) * Simplify code * Use async_config_entry_first_refresh() * Use lambda to get value --- homeassistant/components/nextdns/__init__.py | 7 ++-- homeassistant/components/nextdns/sensor.py | 36 ++++++++++++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 71ddeb9e8eb..7df172da8ab 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -153,8 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (ApiError, ClientConnectorError, asyncio.TimeoutError) as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN].setdefault(entry.entry_id, {}) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} tasks = [] @@ -165,7 +164,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, nextdns, profile_id, update_interval ) tasks.append( - hass.data[DOMAIN][entry.entry_id][coordinator_name].async_refresh() + hass.data[DOMAIN][entry.entry_id][ + coordinator_name + ].async_config_entry_first_refresh() ) await asyncio.gather(*tasks) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 35a15f010b3..fc5a8169526 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -1,8 +1,9 @@ """Support for the NextDNS service.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import cast +from typing import Any, cast from homeassistant.components.sensor import ( SensorEntity, @@ -14,6 +15,7 @@ from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NextDnsUpdateCoordinator @@ -34,6 +36,7 @@ class NextDnsSensorRequiredKeysMixin: """Class for NextDNS entity required keys.""" coordinator_type: str + value: Callable[[Any], StateType] @dataclass @@ -52,6 +55,7 @@ SENSORS = ( name="{profile_name} DNS Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.all_queries, ), NextDnsSensorEntityDescription( key="blocked_queries", @@ -61,6 +65,7 @@ SENSORS = ( name="{profile_name} DNS Queries Blocked", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.blocked_queries, ), NextDnsSensorEntityDescription( key="relayed_queries", @@ -70,6 +75,7 @@ SENSORS = ( name="{profile_name} DNS Queries Relayed", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.relayed_queries, ), NextDnsSensorEntityDescription( key="blocked_queries_ratio", @@ -79,6 +85,7 @@ SENSORS = ( name="{profile_name} DNS Queries Blocked Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.blocked_queries_ratio, ), NextDnsSensorEntityDescription( key="doh_queries", @@ -89,6 +96,7 @@ SENSORS = ( name="{profile_name} DNS-over-HTTPS Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doh_queries, ), NextDnsSensorEntityDescription( key="dot_queries", @@ -99,6 +107,7 @@ SENSORS = ( name="{profile_name} DNS-over-TLS Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.dot_queries, ), NextDnsSensorEntityDescription( key="doq_queries", @@ -109,6 +118,7 @@ SENSORS = ( name="{profile_name} DNS-over-QUIC Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doq_queries, ), NextDnsSensorEntityDescription( key="udp_queries", @@ -119,6 +129,7 @@ SENSORS = ( name="{profile_name} UDP Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.udp_queries, ), NextDnsSensorEntityDescription( key="doh_queries_ratio", @@ -129,6 +140,7 @@ SENSORS = ( name="{profile_name} DNS-over-HTTPS Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doh_queries_ratio, ), NextDnsSensorEntityDescription( key="dot_queries_ratio", @@ -139,6 +151,7 @@ SENSORS = ( name="{profile_name} DNS-over-TLS Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.dot_queries_ratio, ), NextDnsSensorEntityDescription( key="doq_queries_ratio", @@ -149,6 +162,7 @@ SENSORS = ( name="{profile_name} DNS-over-QUIC Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doq_queries_ratio, ), NextDnsSensorEntityDescription( key="udp_queries_ratio", @@ -159,6 +173,7 @@ SENSORS = ( name="{profile_name} UDP Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.udp_queries_ratio, ), NextDnsSensorEntityDescription( key="encrypted_queries", @@ -169,6 +184,7 @@ SENSORS = ( name="{profile_name} Encrypted Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.encrypted_queries, ), NextDnsSensorEntityDescription( key="unencrypted_queries", @@ -179,6 +195,7 @@ SENSORS = ( name="{profile_name} Unencrypted Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.unencrypted_queries, ), NextDnsSensorEntityDescription( key="encrypted_queries_ratio", @@ -189,6 +206,7 @@ SENSORS = ( name="{profile_name} Encrypted Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.encrypted_queries_ratio, ), NextDnsSensorEntityDescription( key="ipv4_queries", @@ -199,6 +217,7 @@ SENSORS = ( name="{profile_name} IPv4 Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.ipv4_queries, ), NextDnsSensorEntityDescription( key="ipv6_queries", @@ -209,6 +228,7 @@ SENSORS = ( name="{profile_name} IPv6 Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.ipv6_queries, ), NextDnsSensorEntityDescription( key="ipv6_queries_ratio", @@ -219,6 +239,7 @@ SENSORS = ( name="{profile_name} IPv6 Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.ipv6_queries_ratio, ), NextDnsSensorEntityDescription( key="validated_queries", @@ -229,6 +250,7 @@ SENSORS = ( name="{profile_name} DNSSEC Validated Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.validated_queries, ), NextDnsSensorEntityDescription( key="not_validated_queries", @@ -239,6 +261,7 @@ SENSORS = ( name="{profile_name} DNSSEC Not Validated Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.not_validated_queries, ), NextDnsSensorEntityDescription( key="validated_queries_ratio", @@ -249,6 +272,7 @@ SENSORS = ( name="{profile_name} DNSSEC Validated Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.validated_queries_ratio, ), ) @@ -278,7 +302,7 @@ class NextDnsSensor(CoordinatorEntity, SensorEntity): def __init__( self, coordinator: NextDnsUpdateCoordinator, - description: SensorEntityDescription, + description: NextDnsSensorEntityDescription, ) -> None: """Initialize.""" super().__init__(coordinator) @@ -287,13 +311,11 @@ class NextDnsSensor(CoordinatorEntity, SensorEntity): self._attr_name = cast(str, description.name).format( profile_name=coordinator.profile_name ) - self._attr_native_value = getattr(coordinator.data, description.key) - self.entity_description = description + self._attr_native_value = description.value(coordinator.data) + self.entity_description: NextDnsSensorEntityDescription = description @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._attr_native_value = getattr( - self.coordinator.data, self.entity_description.key - ) + self._attr_native_value = self.entity_description.value(self.coordinator.data) self.async_write_ha_state() From a70cb8af782deb5f2b8701567f6a7a47a72db5d3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 05:14:56 +0200 Subject: [PATCH 141/820] Add NextDNS diagnostics platform (#74367) Add diagnostics platform --- .../components/nextdns/diagnostics.py | 45 ++++++++++++++ tests/components/nextdns/test_diagnostics.py | 59 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 homeassistant/components/nextdns/diagnostics.py create mode 100644 tests/components/nextdns/test_diagnostics.py diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py new file mode 100644 index 00000000000..4be22684395 --- /dev/null +++ b/homeassistant/components/nextdns/diagnostics.py @@ -0,0 +1,45 @@ +"""Diagnostics support for NextDNS.""" +from __future__ import annotations + +from dataclasses import asdict + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant + +from .const import ( + ATTR_DNSSEC, + ATTR_ENCRYPTION, + ATTR_IP_VERSIONS, + ATTR_PROTOCOLS, + ATTR_STATUS, + CONF_PROFILE_ID, + DOMAIN, +) + +TO_REDACT = {CONF_API_KEY, CONF_PROFILE_ID, CONF_UNIQUE_ID} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + coordinators = hass.data[DOMAIN][config_entry.entry_id] + + dnssec_coordinator = coordinators[ATTR_DNSSEC] + encryption_coordinator = coordinators[ATTR_ENCRYPTION] + ip_versions_coordinator = coordinators[ATTR_IP_VERSIONS] + protocols_coordinator = coordinators[ATTR_PROTOCOLS] + status_coordinator = coordinators[ATTR_STATUS] + + diagnostics_data = { + "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), + "dnssec_coordinator_data": asdict(dnssec_coordinator.data), + "encryption_coordinator_data": asdict(encryption_coordinator.data), + "ip_versions_coordinator_data": asdict(ip_versions_coordinator.data), + "protocols_coordinator_data": asdict(protocols_coordinator.data), + "status_coordinator_data": asdict(status_coordinator.data), + } + + return diagnostics_data diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py new file mode 100644 index 00000000000..73844b6a2e3 --- /dev/null +++ b/tests/components/nextdns/test_diagnostics.py @@ -0,0 +1,59 @@ +"""Test NextDNS diagnostics.""" +from homeassistant.components.diagnostics import REDACTED + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.components.nextdns import init_integration + + +async def test_entry_diagnostics(hass, hass_client): + """Test config entry diagnostics.""" + entry = await init_integration(hass) + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert result["config_entry"] == { + "entry_id": entry.entry_id, + "version": 1, + "domain": "nextdns", + "title": "Fake Profile", + "data": {"profile_id": REDACTED, "api_key": REDACTED}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, + } + assert result["dnssec_coordinator_data"] == { + "not_validated_queries": 25, + "validated_queries": 75, + "validated_queries_ratio": 75.0, + } + assert result["encryption_coordinator_data"] == { + "encrypted_queries": 60, + "unencrypted_queries": 40, + "encrypted_queries_ratio": 60.0, + } + assert result["ip_versions_coordinator_data"] == { + "ipv6_queries": 10, + "ipv4_queries": 90, + "ipv6_queries_ratio": 10.0, + } + assert result["protocols_coordinator_data"] == { + "doh_queries": 20, + "doq_queries": 10, + "dot_queries": 30, + "udp_queries": 40, + "doh_queries_ratio": 22.2, + "doq_queries_ratio": 11.1, + "dot_queries_ratio": 33.3, + "udp_queries_ratio": 44.4, + } + assert result["status_coordinator_data"] == { + "all_queries": 100, + "allowed_queries": 30, + "blocked_queries": 20, + "default_queries": 40, + "relayed_queries": 10, + "blocked_queries_ratio": 20.0, + } From 3d63d4fb36d02cdf2186a377695e81f12767af88 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:00:40 -0500 Subject: [PATCH 142/820] Fix apple tv not coming online if connected before entity created (#74488) --- homeassistant/components/apple_tv/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 45250451f37..5177c6f3486 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -123,6 +123,10 @@ class AppleTVEntity(Entity): self.atv = None self.async_write_ha_state() + if self.manager.atv: + # ATV is already connected + _async_connected(self.manager.atv) + self.async_on_remove( async_dispatcher_connect( self.hass, f"{SIGNAL_CONNECTED}_{self.unique_id}", _async_connected From df6892b90862dea911b9748865e4dbd1342b3fa4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:02:18 -0500 Subject: [PATCH 143/820] Offer HKC local push control option when there are multiple zeroconf homekit matches (#74441) --- homeassistant/components/zeroconf/__init__.py | 9 +++- tests/components/zeroconf/test_init.py | 41 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 29afd5fc236..1bfa44f3894 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -424,11 +424,16 @@ class ZeroconfDiscovery: # Since we prefer local control, if the integration that is being discovered # is cloud AND the homekit device is UNPAIRED we still want to discovery it. # + # Additionally if the integration is polling, HKC offers a local push + # experience for the user to control the device so we want to offer that + # as well. + # # As soon as the device becomes paired, the config flow will be dismissed # in the event the user does not want to pair with Home Assistant. # - if not integration.iot_class or not integration.iot_class.startswith( - "cloud" + if not integration.iot_class or ( + not integration.iot_class.startswith("cloud") + and "polling" not in integration.iot_class ): return diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 88dceb9d464..6bc37e10da2 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -530,7 +530,8 @@ async def test_homekit_match_partial_space(hass, mock_async_zeroconf): await hass.async_block_till_done() assert len(mock_service_browser.mock_calls) == 1 - assert len(mock_config_flow.mock_calls) == 1 + # One for HKC, and one for LIFX since lifx is local polling + assert len(mock_config_flow.mock_calls) == 2 assert mock_config_flow.mock_calls[0][1][0] == "lifx" @@ -741,6 +742,44 @@ async def test_homekit_controller_still_discovered_unpaired_for_cloud( assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" +async def test_homekit_controller_still_discovered_unpaired_for_polling( + hass, mock_async_zeroconf +): + """Test discovery is still passed to homekit controller when unpaired and discovered by polling integration. + + Since we prefer local push, if the integration that is being discovered + is polling AND the homekit device is unpaired we still want to discovery it + """ + with patch.dict( + zc_gen.ZEROCONF, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), patch.dict( + zc_gen.HOMEKIT, + {"iSmartGate": "gogogate2"}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, + "HaAsyncServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._udp.local." + ), + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_homekit_info_mock("iSmartGate", HOMEKIT_STATUS_UNPAIRED), + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[0][1][0] == "gogogate2" + assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" + + async def test_info_from_service_non_utf8(hass): """Test info_from_service handles non UTF-8 property keys and values correctly.""" service_type = "_test._tcp.local." From 3875fc59530d7794f80bb650c0d329499aade113 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:08:39 -0500 Subject: [PATCH 144/820] Cache the response from config/device_registry/list (#74483) --- .../components/config/device_registry.py | 56 ++++++++++++++----- .../components/config/test_device_registry.py | 30 +++++++++- 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index e811d43d502..587710a8c2a 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,12 +1,23 @@ """HTTP views to interact with the device registry.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import loader from homeassistant.components import websocket_api from homeassistant.components.websocket_api.decorators import require_admin -from homeassistant.core import HomeAssistant, callback +from homeassistant.components.websocket_api.messages import ( + IDEN_JSON_TEMPLATE, + IDEN_TEMPLATE, + message_to_json, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceEntryDisabler, async_get +from homeassistant.helpers.device_registry import ( + EVENT_DEVICE_REGISTRY_UPDATED, + DeviceEntryDisabler, + async_get, +) WS_TYPE_LIST = "config/device_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -29,6 +40,36 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( async def async_setup(hass): """Enable the Device Registry views.""" + + cached_list_devices: str | None = None + + @callback + def _async_clear_list_device_cache(event: Event) -> None: + nonlocal cached_list_devices + cached_list_devices = None + + @callback + def websocket_list_devices(hass, connection, msg): + """Handle list devices command.""" + nonlocal cached_list_devices + if not cached_list_devices: + registry = async_get(hass) + cached_list_devices = message_to_json( + websocket_api.result_message( + IDEN_TEMPLATE, + [_entry_dict(entry) for entry in registry.devices.values()], + ) + ) + connection.send_message( + cached_list_devices.replace(IDEN_JSON_TEMPLATE, str(msg["id"]), 1) + ) + + hass.bus.async_listen( + EVENT_DEVICE_REGISTRY_UPDATED, + _async_clear_list_device_cache, + run_immediately=True, + ) + websocket_api.async_register_command( hass, WS_TYPE_LIST, websocket_list_devices, SCHEMA_WS_LIST ) @@ -41,17 +82,6 @@ async def async_setup(hass): return True -@callback -def websocket_list_devices(hass, connection, msg): - """Handle list devices command.""" - registry = async_get(hass) - connection.send_message( - websocket_api.result_message( - msg["id"], [_entry_dict(entry) for entry in registry.devices.values()] - ) - ) - - @require_admin @callback def websocket_update_device(hass, connection, msg): diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index f923b326100..f9289b6e3b3 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -29,14 +29,14 @@ def registry(hass): async def test_list_devices(hass, client, registry): """Test list entries.""" - registry.async_get_or_create( + device1 = registry.async_get_or_create( config_entry_id="1234", connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) - registry.async_get_or_create( + device2 = registry.async_get_or_create( config_entry_id="1234", identifiers={("bridgeid", "1234")}, manufacturer="manufacturer", @@ -85,6 +85,32 @@ async def test_list_devices(hass, client, registry): }, ] + registry.async_remove_device(device2.id) + await hass.async_block_till_done() + + await client.send_json({"id": 6, "type": "config/device_registry/list"}) + msg = await client.receive_json() + + assert msg["result"] == [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": None, + "connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": device1.id, + "identifiers": [["bridgeid", "0123"]], + "manufacturer": "manufacturer", + "model": "model", + "name": None, + "name_by_user": None, + "sw_version": None, + "via_device_id": None, + } + ] + @pytest.mark.parametrize( "payload_key,payload_value", From 8dfb0cb4e7da11c599f90414d0f98ea28a6c2e87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:09:33 -0500 Subject: [PATCH 145/820] Fix SIGN_QUERY_PARAM in check in auth_middleware (#74479) --- homeassistant/components/http/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 09ef6e13e03..18f68cc386f 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -218,7 +218,7 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: # for every request. elif ( request.method == "GET" - and SIGN_QUERY_PARAM in request.query + and SIGN_QUERY_PARAM in request.query_string and await async_validate_signed_request(request) ): authenticated = True From ce35324e7391f0e06c415f414e7a52276ff4d1d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:11:51 -0500 Subject: [PATCH 146/820] Cache the response from config/entity_registry/list (#74443) --- .../components/config/entity_registry.py | 95 +++++++++++-------- .../components/config/test_entity_registry.py | 35 +++++++ 2 files changed, 92 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 5f484c10472..445ca96c8b0 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,11 +1,18 @@ """HTTP views to interact with the entity registry.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import require_admin -from homeassistant.core import callback +from homeassistant.components.websocket_api.messages import ( + IDEN_JSON_TEMPLATE, + IDEN_TEMPLATE, + message_to_json, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -13,8 +20,40 @@ from homeassistant.helpers import ( ) -async def async_setup(hass): +async def async_setup(hass: HomeAssistant) -> bool: """Enable the Entity Registry views.""" + + cached_list_entities: str | None = None + + @callback + def _async_clear_list_entities_cache(event: Event) -> None: + nonlocal cached_list_entities + cached_list_entities = None + + @websocket_api.websocket_command( + {vol.Required("type"): "config/entity_registry/list"} + ) + @callback + def websocket_list_entities(hass, connection, msg): + """Handle list registry entries command.""" + nonlocal cached_list_entities + if not cached_list_entities: + registry = er.async_get(hass) + cached_list_entities = message_to_json( + websocket_api.result_message( + IDEN_TEMPLATE, + [_entry_dict(entry) for entry in registry.entities.values()], + ) + ) + connection.send_message( + cached_list_entities.replace(IDEN_JSON_TEMPLATE, str(msg["id"]), 1) + ) + + hass.bus.async_listen( + er.EVENT_ENTITY_REGISTRY_UPDATED, + _async_clear_list_entities_cache, + run_immediately=True, + ) websocket_api.async_register_command(hass, websocket_list_entities) websocket_api.async_register_command(hass, websocket_get_entity) websocket_api.async_register_command(hass, websocket_update_entity) @@ -22,33 +61,6 @@ async def async_setup(hass): return True -@websocket_api.websocket_command({vol.Required("type"): "config/entity_registry/list"}) -@callback -def websocket_list_entities(hass, connection, msg): - """Handle list registry entries command.""" - registry = er.async_get(hass) - connection.send_message( - websocket_api.result_message( - msg["id"], - [ - { - "area_id": entry.area_id, - "config_entry_id": entry.config_entry_id, - "device_id": entry.device_id, - "disabled_by": entry.disabled_by, - "entity_category": entry.entity_category, - "entity_id": entry.entity_id, - "hidden_by": entry.hidden_by, - "icon": entry.icon, - "name": entry.name, - "platform": entry.platform, - } - for entry in registry.entities.values() - ], - ) - ) - - @websocket_api.websocket_command( { vol.Required("type"): "config/entity_registry/get", @@ -211,7 +223,7 @@ def websocket_remove_entity(hass, connection, msg): @callback -def _entry_ext_dict(entry): +def _entry_dict(entry): """Convert entry to API format.""" return { "area_id": entry.area_id, @@ -224,12 +236,19 @@ def _entry_ext_dict(entry): "icon": entry.icon, "name": entry.name, "platform": entry.platform, - "capabilities": entry.capabilities, - "device_class": entry.device_class, - "has_entity_name": entry.has_entity_name, - "options": entry.options, - "original_device_class": entry.original_device_class, - "original_icon": entry.original_icon, - "original_name": entry.original_name, - "unique_id": entry.unique_id, } + + +@callback +def _entry_ext_dict(entry): + """Convert entry to API format.""" + data = _entry_dict(entry) + data["capabilities"] = entry.capabilities + data["device_class"] = entry.device_class + data["has_entity_name"] = entry.has_entity_name + data["options"] = entry.options + data["original_device_class"] = entry.original_device_class + data["original_icon"] = entry.original_icon + data["original_name"] = entry.original_name + data["unique_id"] = entry.unique_id + return data diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 69744817a27..9c5984f751e 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -5,6 +5,7 @@ from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON from homeassistant.helpers.device_registry import DeviceEntryDisabler from homeassistant.helpers.entity_registry import ( + EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, RegistryEntryDisabler, RegistryEntryHider, @@ -81,6 +82,40 @@ async def test_list_entities(hass, client): }, ] + mock_registry( + hass, + { + "test_domain.name": RegistryEntry( + entity_id="test_domain.name", + unique_id="1234", + platform="test_platform", + name="Hello World", + ), + }, + ) + + hass.bus.async_fire( + EVENT_ENTITY_REGISTRY_UPDATED, + {"action": "create", "entity_id": "test_domain.no_name"}, + ) + await client.send_json({"id": 6, "type": "config/entity_registry/list"}) + msg = await client.receive_json() + + assert msg["result"] == [ + { + "config_entry_id": None, + "device_id": None, + "area_id": None, + "disabled_by": None, + "entity_id": "test_domain.name", + "hidden_by": None, + "name": "Hello World", + "icon": None, + "platform": "test_platform", + "entity_category": None, + }, + ] + async def test_get_entity(hass, client): """Test get entry.""" From 148035c8ca8c6d706ec2ddcc9a52475b086ccb0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Jul 2022 00:52:41 -0500 Subject: [PATCH 147/820] Bump aiohomekit to 0.7.20 (#74489) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a15c576c313..955f5e37177 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.19"], + "requirements": ["aiohomekit==0.7.20"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 82a7b2aa99c..5696bbe1db1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.19 +aiohomekit==0.7.20 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index beda687d9f8..07a71a6915e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.19 +aiohomekit==0.7.20 # homeassistant.components.emulated_hue # homeassistant.components.http From 05416f56aa8f136126c85f72f6d53f93b5c5c3a7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 6 Jul 2022 09:45:30 +0200 Subject: [PATCH 148/820] Use FlowResultType in deCONZ config flow tests (#74495) --- tests/components/deconz/test_config_flow.py | 76 ++++++++++----------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 1a7031a0fd6..2f21081a5ae 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -29,11 +29,7 @@ from homeassistant.config_entries import ( SOURCE_USER, ) from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration @@ -55,14 +51,14 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -75,7 +71,7 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -100,7 +96,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): result["flow_id"], user_input={CONF_HOST: CONF_MANUAL_INPUT} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -108,7 +104,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -127,7 +123,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -148,7 +144,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -156,7 +152,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -175,7 +171,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -192,7 +188,7 @@ async def test_manual_configuration_after_discovery_timeout(hass, aioclient_mock DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -205,7 +201,7 @@ async def test_manual_configuration_after_discovery_ResponseError(hass, aioclien DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -224,7 +220,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -232,7 +228,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -251,7 +247,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" @@ -270,7 +266,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -278,7 +274,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -297,7 +293,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -313,7 +309,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -321,7 +317,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -338,7 +334,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_bridges" @@ -367,7 +363,7 @@ async def test_link_step_fails(hass, aioclient_mock, raised_error, error_string) result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post("http://1.2.3.4:80/api", exc=raised_error) @@ -376,7 +372,7 @@ async def test_link_step_fails(hass, aioclient_mock, raised_error, error_string) result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": error_string} @@ -391,7 +387,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): context={"source": SOURCE_REAUTH}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" new_api_key = "new_key" @@ -412,7 +408,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_API_KEY] == new_api_key @@ -433,7 +429,7 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): context={"source": SOURCE_SSDP}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" flows = hass.config_entries.flow.async_progress() @@ -450,7 +446,7 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -482,7 +478,7 @@ async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 @@ -506,7 +502,7 @@ async def test_ssdp_discovery_dont_update_configuration(hass, aioclient_mock): context={"source": SOURCE_SSDP}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -533,7 +529,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration( context={"source": SOURCE_SSDP}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -553,7 +549,7 @@ async def test_flow_hassio_discovery(hass): ), context={"source": SOURCE_HASSIO}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} @@ -572,7 +568,7 @@ async def test_flow_hassio_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["result"].data == { CONF_HOST: "mock-deconz", CONF_PORT: 80, @@ -603,7 +599,7 @@ async def test_hassio_discovery_update_configuration(hass, aioclient_mock): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert config_entry.data[CONF_PORT] == 8080 @@ -628,7 +624,7 @@ async def test_hassio_discovery_dont_update_configuration(hass, aioclient_mock): context={"source": SOURCE_HASSIO}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -638,7 +634,7 @@ async def test_option_flow(hass, aioclient_mock): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "deconz_devices" result = await hass.config_entries.options.async_configure( @@ -650,7 +646,7 @@ async def test_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_ALLOW_CLIP_SENSOR: False, CONF_ALLOW_DECONZ_GROUPS: False, From 1b37d9cbc6fa255fbbde3335c85ef7ed3fb871ca Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 6 Jul 2022 09:52:41 +0200 Subject: [PATCH 149/820] Use FlowResultType in Devolo Home Control tests (#74490) * Use FlowResultType in devolo Home Control tests * Add return types --- .../devolo_home_control/test_config_flow.py | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index d2a359b9438..4f7140b7980 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -3,8 +3,10 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.devolo_home_control.const import DEFAULT_MYDEVOLO, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult, FlowResultType from .const import ( DISCOVERY_INFO, @@ -15,28 +17,28 @@ from .const import ( from tests.common import MockConfigEntry -async def test_form(hass): +async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} await _setup(hass, result) @pytest.mark.credentials_invalid -async def test_form_invalid_credentials_user(hass): +async def test_form_invalid_credentials_user(hass: HomeAssistant) -> None: """Test if we get the error message on invalid credentials.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -47,7 +49,7 @@ async def test_form_invalid_credentials_user(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_form_already_configured(hass): +async def test_form_already_configured(hass: HomeAssistant) -> None: """Test if we get the error message on already configured.""" with patch( "homeassistant.components.devolo_home_control.Mydevolo.uuid", @@ -59,17 +61,17 @@ async def test_form_already_configured(hass): context={"source": config_entries.SOURCE_USER}, data={"username": "test-username", "password": "test-password"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_form_advanced_options(hass): +async def test_form_advanced_options(hass: HomeAssistant) -> None: """Test if we get the advanced options if user has enabled it.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -100,7 +102,7 @@ async def test_form_advanced_options(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_zeroconf(hass): +async def test_form_zeroconf(hass: HomeAssistant) -> None: """Test that the zeroconf confirmation form is served.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -109,13 +111,13 @@ async def test_form_zeroconf(hass): ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM await _setup(hass, result) @pytest.mark.credentials_invalid -async def test_form_invalid_credentials_zeroconf(hass): +async def test_form_invalid_credentials_zeroconf(hass: HomeAssistant) -> None: """Test if we get the error message on invalid credentials.""" result = await hass.config_entries.flow.async_init( @@ -125,7 +127,7 @@ async def test_form_invalid_credentials_zeroconf(hass): ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -135,7 +137,7 @@ async def test_form_invalid_credentials_zeroconf(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_zeroconf_wrong_device(hass): +async def test_zeroconf_wrong_device(hass: HomeAssistant) -> None: """Test that the zeroconf ignores wrong devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -144,7 +146,7 @@ async def test_zeroconf_wrong_device(hass): ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT result = await hass.config_entries.flow.async_init( DOMAIN, @@ -153,10 +155,10 @@ async def test_zeroconf_wrong_device(hass): ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT -async def test_form_reauth(hass): +async def test_form_reauth(hass: HomeAssistant) -> None: """Test that the reauth confirmation form is served.""" mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) mock_config.add_to_hass(hass) @@ -174,7 +176,7 @@ async def test_form_reauth(hass): ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.devolo_home_control.async_setup_entry", @@ -189,12 +191,12 @@ async def test_form_reauth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.credentials_invalid -async def test_form_invalid_credentials_reauth(hass): +async def test_form_invalid_credentials_reauth(hass: HomeAssistant) -> None: """Test if we get the error message on invalid credentials.""" mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) mock_config.add_to_hass(hass) @@ -219,7 +221,7 @@ async def test_form_invalid_credentials_reauth(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_form_uuid_change_reauth(hass): +async def test_form_uuid_change_reauth(hass: HomeAssistant) -> None: """Test that the reauth confirmation form is served.""" mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) mock_config.add_to_hass(hass) @@ -237,7 +239,7 @@ async def test_form_uuid_change_reauth(hass): ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.devolo_home_control.async_setup_entry", @@ -252,11 +254,11 @@ async def test_form_uuid_change_reauth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "reauth_failed"} -async def _setup(hass, result): +async def _setup(hass: HomeAssistant, result: FlowResult) -> None: """Finish configuration steps.""" with patch( "homeassistant.components.devolo_home_control.async_setup_entry", From ef6fd78ede448ac6d88e92f005b4e9520c6b00e8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 6 Jul 2022 09:54:26 +0200 Subject: [PATCH 150/820] Use FlowResultType in Axis config flow tests (#74496) --- tests/components/axis/test_config_flow.py | 43 ++++++++++------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index f09e87020c3..1459ae215d9 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest import respx -from homeassistant import data_entry_flow from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( @@ -31,11 +30,7 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .test_device import ( DEFAULT_HOST, @@ -57,7 +52,7 @@ async def test_flow_manual_configuration(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with respx.mock: @@ -72,7 +67,7 @@ async def test_flow_manual_configuration(hass): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -93,7 +88,7 @@ async def test_manual_configuration_update_configuration(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -112,7 +107,7 @@ async def test_manual_configuration_update_configuration(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert device.host == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 @@ -124,7 +119,7 @@ async def test_flow_fails_faulty_credentials(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -150,7 +145,7 @@ async def test_flow_fails_cannot_connect(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -187,7 +182,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with respx.mock: @@ -202,7 +197,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -227,7 +222,7 @@ async def test_reauth_flow_update_configuration(hass): data=config_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with respx.mock: @@ -243,7 +238,7 @@ async def test_reauth_flow_update_configuration(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert device.host == "2.3.4.5" assert device.username == "user2" @@ -317,7 +312,7 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER flows = hass.config_entries.flow.async_progress() @@ -336,7 +331,7 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -398,7 +393,7 @@ async def test_discovered_device_already_configured( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == DEFAULT_HOST @@ -467,7 +462,7 @@ async def test_discovery_flow_updated_configuration( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data == { CONF_HOST: "2.3.4.5", @@ -525,7 +520,7 @@ async def test_discovery_flow_ignore_non_axis_device( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_axis_device" @@ -574,7 +569,7 @@ async def test_discovery_flow_ignore_link_local_address( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "link_local_address" @@ -591,7 +586,7 @@ async def test_option_flow(hass): device.config_entry.entry_id ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "configure_stream" assert set(result["data_schema"].schema[CONF_STREAM_PROFILE].container) == { DEFAULT_STREAM_PROFILE, @@ -608,7 +603,7 @@ async def test_option_flow(hass): user_input={CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EVENTS: True, CONF_STREAM_PROFILE: "profile_1", From a27d483009a969cedd6156071811674c1e19b62a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:25:53 +0200 Subject: [PATCH 151/820] Remove unifi from mypy ignore list (#74456) * Remove unifi diagnostics from mypy ignore list * Remove unifi init from mypy ignore list * Remove unifi device tracker from mypy ignore list * Adjust doc string * Adjust doc string * Remove unifi entity base from mypy ignore list * Keep comprehension * Remove unifi config flow from mypy ignore list * Fix circular import --- homeassistant/components/unifi/__init__.py | 8 +++++- homeassistant/components/unifi/config_flow.py | 28 ++++++++----------- .../components/unifi/device_tracker.py | 12 +++----- homeassistant/components/unifi/diagnostics.py | 5 ++-- .../components/unifi/unifi_client.py | 8 ++++-- .../components/unifi/unifi_entity_base.py | 15 +++++++--- mypy.ini | 15 ---------- script/hassfest/mypy_config.py | 5 ---- 8 files changed, 43 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 7a874aff993..1369bb69e1b 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,4 +1,7 @@ """Integration to UniFi Network and its various features.""" +from collections.abc import Mapping +from typing import Any + from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback @@ -92,7 +95,10 @@ async def async_flatten_entry_data( Keep controller key layer in case user rollbacks. """ - data: dict = {**config_entry.data, **config_entry.data[CONF_CONTROLLER]} + data: Mapping[str, Any] = { + **config_entry.data, + **config_entry.data[CONF_CONTROLLER], + } if config_entry.data != data: hass.config_entries.async_update_entry(config_entry, data=data) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index fcf5970bf6c..2f49c15e4d8 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -46,7 +46,7 @@ from .const import ( DEFAULT_POE_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) -from .controller import get_controller +from .controller import UniFiController, get_controller from .errors import AuthenticationRequired, CannotConnect DEFAULT_PORT = 443 @@ -75,11 +75,11 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def __init__(self) -> None: """Initialize the UniFi Network flow.""" - self.config = {} - self.site_ids = {} - self.site_names = {} - self.reauth_config_entry = None - self.reauth_schema = {} + self.config: dict[str, Any] = {} + self.site_ids: dict[str, str] = {} + self.site_names: dict[str, str] = {} + self.reauth_config_entry: config_entries.ConfigEntry | None = None + self.reauth_schema: dict[vol.Marker, Any] = {} async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -156,8 +156,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Select site to control.""" - errors = {} - if user_input is not None: unique_id = user_input[CONF_SITE_ID] @@ -173,9 +171,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): abort_reason = "reauth_successful" if config_entry: - controller = self.hass.data.get(UNIFI_DOMAIN, {}).get( - config_entry.entry_id - ) + controller: UniFiController | None = self.hass.data.get( + UNIFI_DOMAIN, {} + ).get(config_entry.entry_id) if controller and controller.available: return self.async_abort(reason="already_configured") @@ -199,7 +197,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): data_schema=vol.Schema( {vol.Required(CONF_SITE_ID): vol.In(self.site_names)} ), - errors=errors, ) async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: @@ -207,6 +204,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): config_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) + assert config_entry self.reauth_config_entry = config_entry self.context["title_placeholders"] = { @@ -258,11 +256,12 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): class UnifiOptionsFlowHandler(config_entries.OptionsFlow): """Handle Unifi Network options.""" + controller: UniFiController + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize UniFi Network options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) - self.controller = None async def async_step_init( self, user_input: dict[str, Any] | None = None @@ -379,8 +378,6 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage configuration of network access controlled clients.""" - errors = {} - if user_input is not None: self.options.update(user_input) return await self.async_step_statistics_sensors() @@ -417,7 +414,6 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): ): bool, } ), - errors=errors, last_step=False, ) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b2070362d02..f2c2230b9e0 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -27,7 +27,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN -from .unifi_client import UniFiClient +from .controller import UniFiController +from .unifi_client import UniFiClientBase from .unifi_entity_base import UniFiBase LOGGER = logging.getLogger(__name__) @@ -79,7 +80,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up device tracker for UniFi Network integration.""" - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = {CLIENT_TRACKER: set(), DEVICE_TRACKER: set()} @callback @@ -144,7 +145,7 @@ def add_device_entities(controller, async_add_entities, devices): async_add_entities(trackers) -class UniFiClientTracker(UniFiClient, ScannerEntity): +class UniFiClientTracker(UniFiClientBase, ScannerEntity): """Representation of a network client.""" DOMAIN = DOMAIN @@ -261,11 +262,6 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): self.async_write_ha_state() self._async_log_debug_data("make_disconnected") - @property - def device_info(self) -> None: - """Return no device info.""" - return None - @property def is_connected(self): """Return true if the client is connected to the network.""" diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index ed059856881..b35fd520ab0 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import format_mac from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN +from .controller import UniFiController TO_REDACT = {CONF_CONTROLLER, CONF_PASSWORD} REDACT_CONFIG = {CONF_CONTROLLER, CONF_HOST, CONF_PASSWORD, CONF_USERNAME} @@ -56,7 +57,7 @@ def async_replace_list_data( """Redact sensitive data in a list.""" redacted = [] for item in data: - new_value = None + new_value: Any | None = None if isinstance(item, (list, set, tuple)): new_value = async_replace_list_data(item, to_replace) elif isinstance(item, Mapping): @@ -74,7 +75,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] diag: dict[str, Any] = {} macs_to_redact: dict[str, str] = {} diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index 9e90eef518a..82aece81b6d 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -5,8 +5,8 @@ from homeassistant.helpers.entity import DeviceInfo from .unifi_entity_base import UniFiBase -class UniFiClient(UniFiBase): - """Base class for UniFi clients.""" +class UniFiClientBase(UniFiBase): + """Base class for UniFi clients (without device info).""" def __init__(self, client, controller) -> None: """Set up client.""" @@ -44,6 +44,10 @@ class UniFiClient(UniFiBase): """Return if controller is available.""" return self.controller.available + +class UniFiClient(UniFiClientBase): + """Base class for UniFi clients (with device info).""" + @property def device_info(self) -> DeviceInfo: """Return a client description for device registry.""" diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index c611fc1ee60..466764714cf 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -1,12 +1,18 @@ """Base class for UniFi Network entities.""" +from __future__ import annotations + +from collections.abc import Callable import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.core import callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +if TYPE_CHECKING: + from .controller import UniFiController + _LOGGER = logging.getLogger(__name__) @@ -16,7 +22,7 @@ class UniFiBase(Entity): DOMAIN = "" TYPE = "" - def __init__(self, item, controller) -> None: + def __init__(self, item, controller: UniFiController) -> None: """Set up UniFi Network entity base. Register mac to controller entities to cover disabled entities. @@ -38,11 +44,12 @@ class UniFiBase(Entity): self.entity_id, self.key, ) - for signal, method in ( + signals: tuple[tuple[str, Callable[..., Any]], ...] = ( (self.controller.signal_reachable, self.async_signal_reachable_callback), (self.controller.signal_options_update, self.options_updated), (self.controller.signal_remove, self.remove_item), - ): + ) + for signal, method in signals: self.async_on_remove(async_dispatcher_connect(self.hass, signal, method)) self._item.register_callback(self.async_update_callback) diff --git a/mypy.ini b/mypy.ini index 52c4dce061f..08cd36dfaea 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2890,21 +2890,6 @@ ignore_errors = true [mypy-homeassistant.components.toon.models] ignore_errors = true -[mypy-homeassistant.components.unifi] -ignore_errors = true - -[mypy-homeassistant.components.unifi.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.unifi.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.unifi.diagnostics] -ignore_errors = true - -[mypy-homeassistant.components.unifi.unifi_entity_base] -ignore_errors = true - [mypy-homeassistant.components.withings] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 521c98ea93d..69e9d8fafa4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -105,11 +105,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.toon", "homeassistant.components.toon.config_flow", "homeassistant.components.toon.models", - "homeassistant.components.unifi", - "homeassistant.components.unifi.config_flow", - "homeassistant.components.unifi.device_tracker", - "homeassistant.components.unifi.diagnostics", - "homeassistant.components.unifi.unifi_entity_base", "homeassistant.components.withings", "homeassistant.components.withings.binary_sensor", "homeassistant.components.withings.common", From 8fb9b45e422df75b3c54a4373e1e809af01a2604 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:26:24 +0200 Subject: [PATCH 152/820] Remove input_datetime from mypy ignore list (#74447) * Remove input_datetime from mypy ignore list * Use assert * Use cast * Use common logic for initial parsing --- .../components/input_datetime/__init__.py | 41 +++++++++---------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index fe034a8edb5..2a64c8d3b89 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import datetime as py_datetime import logging +from typing import Any import voluptuous as vol @@ -84,23 +85,32 @@ def has_date_or_time(conf): raise vol.Invalid("Entity needs at least a date or a time") -def valid_initial(conf): +def valid_initial(conf: dict[str, Any]) -> dict[str, Any]: """Check the initial value is valid.""" - if not (initial := conf.get(CONF_INITIAL)): + if not (conf.get(CONF_INITIAL)): return conf + # Ensure we can parse the initial value, raise vol.Invalid on failure + parse_initial_datetime(conf) + return conf + + +def parse_initial_datetime(conf: dict[str, Any]) -> py_datetime.datetime: + """Check the initial value is valid.""" + initial: str = conf[CONF_INITIAL] + if conf[CONF_HAS_DATE] and conf[CONF_HAS_TIME]: - if dt_util.parse_datetime(initial) is not None: - return conf + if (datetime := dt_util.parse_datetime(initial)) is not None: + return datetime raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a datetime") if conf[CONF_HAS_DATE]: - if dt_util.parse_date(initial) is not None: - return conf + if (date := dt_util.parse_date(initial)) is not None: + return py_datetime.datetime.combine(date, DEFAULT_TIME) raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a date") - if dt_util.parse_time(initial) is not None: - return conf + if (time := dt_util.parse_time(initial)) is not None: + return py_datetime.datetime.combine(py_datetime.date.today(), time) raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a time") @@ -230,21 +240,10 @@ class InputDatetime(RestoreEntity): self.editable = True self._current_datetime = None - if not (initial := config.get(CONF_INITIAL)): + if not config.get(CONF_INITIAL): return - if self.has_date and self.has_time: - current_datetime = dt_util.parse_datetime(initial) - - elif self.has_date: - date = dt_util.parse_date(initial) - current_datetime = py_datetime.datetime.combine(date, DEFAULT_TIME) - - else: - time = dt_util.parse_time(initial) - current_datetime = py_datetime.datetime.combine( - py_datetime.date.today(), time - ) + current_datetime = parse_initial_datetime(config) # If the user passed in an initial value with a timezone, convert it to right tz if current_datetime.tzinfo is not None: diff --git a/mypy.ini b/mypy.ini index 08cd36dfaea..38cb7d4c7f0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2716,9 +2716,6 @@ ignore_errors = true [mypy-homeassistant.components.influxdb] ignore_errors = true -[mypy-homeassistant.components.input_datetime] -ignore_errors = true - [mypy-homeassistant.components.izone.climate] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 69e9d8fafa4..d540ed61cd8 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -47,7 +47,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.icloud.device_tracker", "homeassistant.components.icloud.sensor", "homeassistant.components.influxdb", - "homeassistant.components.input_datetime", "homeassistant.components.izone.climate", "homeassistant.components.konnected", "homeassistant.components.konnected.config_flow", From 8b97271c26cec1075d6c4a914d4f0e6af4436df7 Mon Sep 17 00:00:00 2001 From: Lerosen Date: Wed, 6 Jul 2022 11:21:15 +0200 Subject: [PATCH 153/820] Telegram bot map user data for callback query (#74302) fix(component/telegram-bot): map user data for callback query --- .../components/telegram_bot/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index be1c8325c5f..f4738334745 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -19,6 +19,7 @@ from telegram import ( ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, + User, ) from telegram.error import TelegramError from telegram.ext import CallbackContext, Filters @@ -966,16 +967,17 @@ class BaseTelegramBotEntity: event_data[ATTR_TEXT] = message.text if message.from_user: - event_data.update( - { - ATTR_USER_ID: message.from_user.id, - ATTR_FROM_FIRST: message.from_user.first_name, - ATTR_FROM_LAST: message.from_user.last_name, - } - ) + event_data.update(self._get_user_event_data(message.from_user)) return event_type, event_data + def _get_user_event_data(self, user: User) -> dict[str, Any]: + return { + ATTR_USER_ID: user.id, + ATTR_FROM_FIRST: user.first_name, + ATTR_FROM_LAST: user.last_name, + } + def _get_callback_query_event_data( self, callback_query: CallbackQuery ) -> tuple[str, dict[str, Any]]: @@ -991,6 +993,9 @@ class BaseTelegramBotEntity: event_data[ATTR_MSG] = callback_query.message.to_dict() event_data[ATTR_CHAT_ID] = callback_query.message.chat.id + if callback_query.from_user: + event_data.update(self._get_user_event_data(callback_query.from_user)) + # Split data into command and args if possible event_data.update(self._get_command_event_data(callback_query.data)) From c4855909fa4907dafef7dc6185bd74f2fc144c2e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 6 Jul 2022 13:35:25 +0200 Subject: [PATCH 154/820] Bump aioslimproto to 2.1.1 (#74499) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index 23b3198d7e4..1e076046b44 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==2.0.1"], + "requirements": ["aioslimproto==2.1.1"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5696bbe1db1..4350315268d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -250,7 +250,7 @@ aioshelly==2.0.0 aioskybell==22.6.1 # homeassistant.components.slimproto -aioslimproto==2.0.1 +aioslimproto==2.1.1 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07a71a6915e..882637eab89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -219,7 +219,7 @@ aioshelly==2.0.0 aioskybell==22.6.1 # homeassistant.components.slimproto -aioslimproto==2.0.1 +aioslimproto==2.1.1 # homeassistant.components.steamist aiosteamist==0.3.2 From 8ccb008834638b847643b8a78b4c605b421d6bb2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 14:29:26 +0200 Subject: [PATCH 155/820] Address NextDNS late review (#74503) * Fix coordinator type * Remove pylint disable --- homeassistant/components/nextdns/sensor.py | 4 +--- homeassistant/components/nextdns/system_health.py | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index fc5a8169526..ef0176b071a 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -294,11 +294,9 @@ async def async_setup_entry( async_add_entities(sensors) -class NextDnsSensor(CoordinatorEntity, SensorEntity): +class NextDnsSensor(CoordinatorEntity[NextDnsUpdateCoordinator], SensorEntity): """Define an NextDNS sensor.""" - coordinator: NextDnsUpdateCoordinator - def __init__( self, coordinator: NextDnsUpdateCoordinator, diff --git a/homeassistant/components/nextdns/system_health.py b/homeassistant/components/nextdns/system_health.py index 0fa31e75f1e..a56a89914b8 100644 --- a/homeassistant/components/nextdns/system_health.py +++ b/homeassistant/components/nextdns/system_health.py @@ -10,9 +10,8 @@ from homeassistant.core import HomeAssistant, callback @callback -def async_register( # pylint:disable=unused-argument - hass: HomeAssistant, - register: system_health.SystemHealthRegistration, +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration ) -> None: """Register system health callbacks.""" register.async_register_info(system_health_info) From 47048e4df4a2e1128a0c5258f993a1e26416c413 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jul 2022 16:22:45 +0200 Subject: [PATCH 156/820] Migrate aemet weather to native_* (#74494) --- homeassistant/components/aemet/const.py | 48 ++++++------- homeassistant/components/aemet/sensor.py | 20 +++--- homeassistant/components/aemet/weather.py | 69 ++++++++++++++++--- .../aemet/weather_update_coordinator.py | 46 ++++++------- 4 files changed, 114 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 4be90011f5a..645c1ad0ea2 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -17,14 +17,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_RAINY, ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ) from homeassistant.const import ( DEGREE, @@ -45,8 +37,16 @@ ENTRY_NAME = "name" ENTRY_WEATHER_COORDINATOR = "weather_coordinator" ATTR_API_CONDITION = "condition" +ATTR_API_FORECAST_CONDITION = "condition" ATTR_API_FORECAST_DAILY = "forecast-daily" ATTR_API_FORECAST_HOURLY = "forecast-hourly" +ATTR_API_FORECAST_PRECIPITATION = "precipitation" +ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_API_FORECAST_TEMP = "temperature" +ATTR_API_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_TIME = "datetime" +ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" +ATTR_API_FORECAST_WIND_SPEED = "wind_speed" ATTR_API_HUMIDITY = "humidity" ATTR_API_PRESSURE = "pressure" ATTR_API_RAIN = "rain" @@ -158,14 +158,14 @@ CONDITIONS_MAP = { } FORECAST_MONITORED_CONDITIONS = [ - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -202,43 +202,43 @@ FORECAST_MODE_ATTR_API = { FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=ATTR_FORECAST_CONDITION, + key=ATTR_API_FORECAST_CONDITION, name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_API_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, + key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, name="Precipitation probability", native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_API_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_API_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TIME, + key=ATTR_API_FORECAST_TIME, name="Time", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_BEARING, + key=ATTR_API_FORECAST_WIND_BEARING, name="Wind bearing", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_SPEED, + key=ATTR_API_FORECAST_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f98e3fff49e..e34583148e1 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import ( - ATTR_FORECAST_TIME, + ATTR_API_FORECAST_TIME, ATTRIBUTION, DOMAIN, ENTRY_NAME, @@ -45,17 +45,13 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - name_prefix, - unique_id_prefix, + f"{domain_data[ENTRY_NAME]} {mode} Forecast", + f"{unique_id}-forecast-{mode}", weather_coordinator, mode, description, ) for mode in FORECAST_MODES - if ( - (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") - and (unique_id_prefix := f"{unique_id}-forecast-{mode}") - ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -89,14 +85,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -113,7 +109,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -121,7 +117,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -139,6 +135,6 @@ class AemetForecastSensor(AbstractAemetSensor): ) if forecasts: forecast = forecasts[0].get(self.entity_description.key) - if self.entity_description.key == ATTR_FORECAST_TIME: + if self.entity_description.key == ATTR_API_FORECAST_TIME: forecast = dt_util.parse_datetime(forecast) return forecast diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index d05442b621e..a7ff3630e78 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,13 +1,36 @@ """Support for the AEMET OpenData service.""" -from homeassistant.components.weather import WeatherEntity +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + WeatherEntity, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_API_CONDITION, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, @@ -19,10 +42,32 @@ from .const import ( ENTRY_WEATHER_COORDINATOR, FORECAST_MODE_ATTR_API, FORECAST_MODE_DAILY, + FORECAST_MODE_HOURLY, FORECAST_MODES, ) from .weather_update_coordinator import WeatherUpdateCoordinator +FORECAST_MAP = { + FORECAST_MODE_DAILY: { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, + }, + FORECAST_MODE_HOURLY: { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, + }, +} + async def async_setup_entry( hass: HomeAssistant, @@ -47,9 +92,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_temperature_unit = TEMP_CELSIUS - _attr_pressure_unit = PRESSURE_HPA - _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -75,7 +121,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + forecast_map = FORECAST_MAP[self._forecast_mode] + return [ + {ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()} + for forecast in forecasts + ] @property def humidity(self): @@ -83,12 +134,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -98,6 +149,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index c86465ea8f1..1c64206891c 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -42,23 +42,21 @@ from aemet_opendata.helpers import ( ) import async_timeout -from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, -) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util from .const import ( ATTR_API_CONDITION, + ATTR_API_FORECAST_CONDITION, ATTR_API_FORECAST_DAILY, ATTR_API_FORECAST_HOURLY, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_RAIN, @@ -402,15 +400,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return None return { - ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( + ATTR_API_FORECAST_CONDITION: condition, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), - ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), - ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), + ATTR_API_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_API_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_API_FORECAST_TIME: dt_util.as_utc(date).isoformat(), + ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } def _convert_forecast_hour(self, date, day, hour): @@ -420,15 +418,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): forecast_dt = date.replace(hour=hour, minute=0, second=0) return { - ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( + ATTR_API_FORECAST_CONDITION: condition, + ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_TEMP: self._get_temperature(day, hour), - ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), - ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), + ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), + ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } def _calc_precipitation(self, day, hour): From 41fd1a24bb0823b4a24a0132bf994f32dbd6e84d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 16:29:53 +0200 Subject: [PATCH 157/820] Add NextDNS button platform (#74492) * Add button platform * Add button tests * Fix typo * Use Platform enum * Fix coordinator type --- homeassistant/components/nextdns/__init__.py | 4 +- homeassistant/components/nextdns/button.py | 58 ++++++++++++++++++++ tests/components/nextdns/test_button.py | 47 ++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/nextdns/button.py create mode 100644 tests/components/nextdns/test_button.py diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 7df172da8ab..f9cb1f60cd1 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -20,7 +20,7 @@ from nextdns import ( from nextdns.model import NextDnsData from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -131,7 +131,7 @@ class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator): _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR] COORDINATORS = [ (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), diff --git a/homeassistant/components/nextdns/button.py b/homeassistant/components/nextdns/button.py new file mode 100644 index 00000000000..efe742a6113 --- /dev/null +++ b/homeassistant/components/nextdns/button.py @@ -0,0 +1,58 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from typing import cast + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsStatusUpdateCoordinator +from .const import ATTR_STATUS, DOMAIN + +PARALLEL_UPDATES = 1 + +CLEAR_LOGS_BUTTON = ButtonEntityDescription( + key="clear_logs", + name="{profile_name} Clear Logs", + entity_category=EntityCategory.CONFIG, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add aNextDNS entities from a config_entry.""" + coordinator: NextDnsStatusUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + ATTR_STATUS + ] + + buttons: list[NextDnsButton] = [] + buttons.append(NextDnsButton(coordinator, CLEAR_LOGS_BUTTON)) + + async_add_entities(buttons) + + +class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEntity): + """Define an NextDNS button.""" + + def __init__( + self, + coordinator: NextDnsStatusUpdateCoordinator, + description: ButtonEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_name = cast(str, description.name).format( + profile_name=coordinator.profile_name + ) + self.entity_description = description + + async def async_press(self) -> None: + """Trigger cleaning logs.""" + await self.coordinator.nextdns.clear_logs(self.coordinator.profile_id) diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py new file mode 100644 index 00000000000..39201a668a4 --- /dev/null +++ b/tests/components/nextdns/test_button.py @@ -0,0 +1,47 @@ +"""Test button of NextDNS integration.""" +from unittest.mock import patch + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from . import init_integration + + +async def test_button(hass): + """Test states of the button.""" + registry = er.async_get(hass) + + await init_integration(hass) + + state = hass.states.get("button.fake_profile_clear_logs") + assert state + assert state.state == STATE_UNKNOWN + + entry = registry.async_get("button.fake_profile_clear_logs") + assert entry + assert entry.unique_id == "xyz12_clear_logs" + + +async def test_button_press(hass): + """Test button press.""" + await init_integration(hass) + + now = dt_util.utcnow() + with patch( + "homeassistant.components.nextdns.NextDns.clear_logs" + ) as mock_clear_logs, patch("homeassistant.core.dt_util.utcnow", return_value=now): + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {ATTR_ENTITY_ID: "button.fake_profile_clear_logs"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_clear_logs.assert_called_once() + + state = hass.states.get("button.fake_profile_clear_logs") + assert state + assert state.state == now.isoformat() From 85dac3d47ec9e04690d429c9dbb216917aedb77b Mon Sep 17 00:00:00 2001 From: Gyosa3 <51777889+Gyosa3@users.noreply.github.com> Date: Wed, 6 Jul 2022 17:48:12 +0200 Subject: [PATCH 158/820] Add new alias for valid Celcius temperature units in Tuya (#74511) --- homeassistant/components/tuya/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 8a3e59b1ac9..727e505200b 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -579,7 +579,7 @@ UNITS = ( ), UnitOfMeasurement( unit=TEMP_CELSIUS, - aliases={"°c", "c", "celsius"}, + aliases={"°c", "c", "celsius", "℃"}, device_classes={SensorDeviceClass.TEMPERATURE}, ), UnitOfMeasurement( From d6df26465fa45c17e0433b79425b28ca2401efe7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jul 2022 17:49:06 +0200 Subject: [PATCH 159/820] Fix openweathermap forecast sensors (#74513) --- .../components/openweathermap/const.py | 30 ++++++------- .../components/openweathermap/weather.py | 41 +++++++++++++++++- .../weather_update_coordinator.py | 42 +++++++++---------- 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 06f13daa9c2..836a56c70b2 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -21,9 +21,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, ) from homeassistant.const import ( DEGREE, @@ -68,10 +65,15 @@ ATTR_API_FORECAST = "forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] -ATTR_FORECAST_PRECIPITATION = "precipitation" -ATTR_FORECAST_PRESSURE = "pressure" -ATTR_FORECAST_TEMP = "temperature" -ATTR_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_CONDITION = "condition" +ATTR_API_FORECAST_PRECIPITATION = "precipitation" +ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_API_FORECAST_PRESSURE = "pressure" +ATTR_API_FORECAST_TEMP = "temperature" +ATTR_API_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_TIME = "datetime" +ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" +ATTR_API_FORECAST_WIND_SPEED = "wind_speed" FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" @@ -263,39 +265,39 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=ATTR_FORECAST_CONDITION, + key=ATTR_API_FORECAST_CONDITION, name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_API_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=LENGTH_MILLIMETERS, ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, + key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, name="Precipitation probability", native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_PRESSURE, + key=ATTR_API_FORECAST_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_API_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_API_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TIME, + key=ATTR_API_FORECAST_TIME, name="Time", device_class=SensorDeviceClass.TIMESTAMP, ), diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index fce6efdf3c5..ea439a35586 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,7 +1,20 @@ """Support for the OpenWeatherMap (OWM) service.""" from __future__ import annotations -from homeassistant.components.weather import Forecast, WeatherEntity +from typing import cast + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + Forecast, + WeatherEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( LENGTH_MILLIMETERS, @@ -17,6 +30,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_API_CONDITION, ATTR_API_FORECAST, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, @@ -31,6 +52,17 @@ from .const import ( ) from .weather_update_coordinator import WeatherUpdateCoordinator +FORECAST_MAP = { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, +} + async def async_setup_entry( hass: HomeAssistant, @@ -109,7 +141,12 @@ class OpenWeatherMapWeather(WeatherEntity): @property def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - return self._weather_coordinator.data[ATTR_API_FORECAST] + api_forecasts = self._weather_coordinator.data[ATTR_API_FORECAST] + forecasts = [ + {ha_key: forecast[api_key] for api_key, ha_key in FORECAST_MAP.items()} + for forecast in api_forecasts + ] + return cast(list[Forecast], forecasts) @property def available(self) -> bool: diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 36511424737..98c3b56fb5e 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -8,15 +8,6 @@ from pyowm.commons.exceptions import APIRequestError, UnauthorizedError from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_PRESSURE, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, ) from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -29,6 +20,15 @@ from .const import ( ATTR_API_DEW_POINT, ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_FORECAST, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRESSURE, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRECIPITATION_KIND, ATTR_API_PRESSURE, @@ -158,19 +158,19 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _convert_forecast(self, entry): """Convert the forecast data.""" forecast = { - ATTR_FORECAST_TIME: dt.utc_from_timestamp( + ATTR_API_FORECAST_TIME: dt.utc_from_timestamp( entry.reference_time("unix") ).isoformat(), - ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation( + ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ( round(entry.precipitation_probability * 100) ), - ATTR_FORECAST_NATIVE_PRESSURE: entry.pressure.get("press"), - ATTR_FORECAST_NATIVE_WIND_SPEED: entry.wind().get("speed"), - ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"), - ATTR_FORECAST_CONDITION: self._get_condition( + ATTR_API_FORECAST_PRESSURE: entry.pressure.get("press"), + ATTR_API_FORECAST_WIND_SPEED: entry.wind().get("speed"), + ATTR_API_FORECAST_WIND_BEARING: entry.wind().get("deg"), + ATTR_API_FORECAST_CONDITION: self._get_condition( entry.weather_code, entry.reference_time("unix") ), ATTR_API_CLOUDS: entry.clouds, @@ -178,16 +178,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): temperature_dict = entry.temperature("celsius") if "max" in temperature_dict and "min" in temperature_dict: - forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( - "max" - ) - forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = entry.temperature("celsius").get( + forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("max") + forecast[ATTR_API_FORECAST_TEMP_LOW] = entry.temperature("celsius").get( "min" ) else: - forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( - "temp" - ) + forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("temp") return forecast From d3d2e250902dfe8cf47d28faea6f7189fc5376e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 18:34:51 +0200 Subject: [PATCH 160/820] Update homematicip to 1.0.3 (#74516) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b13c8ca19b2..40f7e67fd07 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.2"], + "requirements": ["homematicip==1.0.3"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 4350315268d..26f05cc1505 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,7 +834,7 @@ home-assistant-frontend==20220705.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.2 +homematicip==1.0.3 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 882637eab89..6a3f3bf5fd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -604,7 +604,7 @@ home-assistant-frontend==20220705.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.2 +homematicip==1.0.3 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From 113ccfe6afc5726a290e698f6dcc81929e4c0fd2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 19:31:57 +0200 Subject: [PATCH 161/820] Update Home Assistant Frontend to 20220706.0 (#74520) Bump Home Assistant Frontend to 20220706.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6b378fe1098..85ead380485 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220705.0"], + "requirements": ["home-assistant-frontend==20220706.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e05a9e7e800..8795e570216 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 26f05cc1505..b63bb91e20f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a3f3bf5fd5..876944367d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 332cf3cd2d674bcf27c7d381f575d69f440355c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Jul 2022 13:49:48 -0500 Subject: [PATCH 162/820] Resolve and caches paths for CachingStaticResource in the executor (#74474) --- homeassistant/components/http/static.py | 48 +++++++++++++------ .../components/recorder/manifest.json | 2 +- pyproject.toml | 1 + requirements.txt | 1 + requirements_all.txt | 3 -- requirements_test_all.txt | 3 -- tests/components/frontend/test_init.py | 32 +++++++++++++ 7 files changed, 69 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 112549553eb..e5e84ca141d 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -9,11 +9,31 @@ from aiohttp import hdrs from aiohttp.web import FileResponse, Request, StreamResponse from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound from aiohttp.web_urldispatcher import StaticResource +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant.core import HomeAssistant + +from .const import KEY_HASS CACHE_TIME: Final = 31 * 86400 # = 1 month CACHE_HEADERS: Final[Mapping[str, str]] = { hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}" } +PATH_CACHE = LRU(512) + + +def _get_file_path( + filename: str, directory: Path, follow_symlinks: bool +) -> Path | None: + filepath = directory.joinpath(filename).resolve() + if not follow_symlinks: + filepath.relative_to(directory) + # on opening a dir, load its contents if allowed + if filepath.is_dir(): + return None + if filepath.is_file(): + return filepath + raise HTTPNotFound class CachingStaticResource(StaticResource): @@ -21,16 +41,19 @@ class CachingStaticResource(StaticResource): async def _handle(self, request: Request) -> StreamResponse: rel_url = request.match_info["filename"] + hass: HomeAssistant = request.app[KEY_HASS] + filename = Path(rel_url) + if filename.anchor: + # rel_url is an absolute name like + # /static/\\machine_name\c$ or /static/D:\path + # where the static dir is totally different + raise HTTPForbidden() try: - filename = Path(rel_url) - if filename.anchor: - # rel_url is an absolute name like - # /static/\\machine_name\c$ or /static/D:\path - # where the static dir is totally different - raise HTTPForbidden() - filepath = self._directory.joinpath(filename).resolve() - if not self._follow_symlinks: - filepath.relative_to(self._directory) + key = (filename, self._directory, self._follow_symlinks) + if (filepath := PATH_CACHE.get(key)) is None: + filepath = PATH_CACHE[key] = await hass.async_add_executor_job( + _get_file_path, filename, self._directory, self._follow_symlinks + ) except (ValueError, FileNotFoundError) as error: # relatively safe raise HTTPNotFound() from error @@ -39,13 +62,10 @@ class CachingStaticResource(StaticResource): request.app.logger.exception(error) raise HTTPNotFound() from error - # on opening a dir, load its contents if allowed - if filepath.is_dir(): - return await super()._handle(request) - if filepath.is_file(): + if filepath: return FileResponse( filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS, ) - raise HTTPNotFound + return await super()._handle(request) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 3d22781906a..f0c1f81689a 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0", "lru-dict==1.1.7"], + "requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/pyproject.toml b/pyproject.toml index 51f1d115e7f..59ad04c4b11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "httpx==0.23.0", "ifaddr==0.1.7", "jinja2==3.1.2", + "lru-dict==1.1.7", "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", diff --git a/requirements.txt b/requirements.txt index 98b148fa923..a8ec77333e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ ciso8601==2.2.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 +lru-dict==1.1.7 PyJWT==2.4.0 cryptography==36.0.2 orjson==3.7.5 diff --git a/requirements_all.txt b/requirements_all.txt index b63bb91e20f..f4895707f06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,9 +974,6 @@ logi_circle==0.2.3 # homeassistant.components.london_underground london-tube-status==0.5 -# homeassistant.components.recorder -lru-dict==1.1.7 - # homeassistant.components.luftdaten luftdaten==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 876944367d9..d5c616edf2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -681,9 +681,6 @@ life360==4.1.1 # homeassistant.components.logi_circle logi_circle==0.2.3 -# homeassistant.components.recorder -lru-dict==1.1.7 - # homeassistant.components.luftdaten luftdaten==0.7.2 diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 84ca04df3ba..661b3ace38a 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -578,3 +578,35 @@ async def test_manifest_json(hass, frontend_themes, mock_http_client): json = await resp.json() assert json["theme_color"] != DEFAULT_THEME_COLOR + + +async def test_static_path_cache(hass, mock_http_client): + """Test static paths cache.""" + resp = await mock_http_client.get("/lovelace/default_view", allow_redirects=False) + assert resp.status == 404 + + resp = await mock_http_client.get("/frontend_latest/", allow_redirects=False) + assert resp.status == 403 + + resp = await mock_http_client.get( + "/static/icons/favicon.ico", allow_redirects=False + ) + assert resp.status == 200 + + # and again to make sure the cache works + resp = await mock_http_client.get( + "/static/icons/favicon.ico", allow_redirects=False + ) + assert resp.status == 200 + + resp = await mock_http_client.get( + "/static/fonts/roboto/Roboto-Bold.woff2", allow_redirects=False + ) + assert resp.status == 200 + + resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False) + assert resp.status == 404 + + # and again to make sure the cache works + resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False) + assert resp.status == 404 From 5e63a44e710e128f7ff55763676d323f16ee76bf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 6 Jul 2022 21:45:54 +0200 Subject: [PATCH 163/820] Remove home_plus_control from mypy ignore list (#74448) --- homeassistant/components/home_plus_control/__init__.py | 3 ++- homeassistant/components/home_plus_control/api.py | 3 +++ mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index 78e31c83caa..e222b65e293 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -83,7 +83,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = hass_entry_data[API] = HomePlusControlAsyncApi(hass, entry, implementation) # Set of entity unique identifiers of this integration - uids = hass_entry_data[ENTITY_UIDS] = set() + uids: set[str] = set() + hass_entry_data[ENTITY_UIDS] = uids # Integration dispatchers hass_entry_data[DISPATCHER_REMOVERS] = [] diff --git a/homeassistant/components/home_plus_control/api.py b/homeassistant/components/home_plus_control/api.py index d9db95323de..9f092b28920 100644 --- a/homeassistant/components/home_plus_control/api.py +++ b/homeassistant/components/home_plus_control/api.py @@ -5,6 +5,7 @@ from homeassistant import config_entries, core from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from .const import DEFAULT_UPDATE_INTERVALS +from .helpers import HomePlusControlOAuth2Implementation class HomePlusControlAsyncApi(HomePlusControlAPI): @@ -40,6 +41,8 @@ class HomePlusControlAsyncApi(HomePlusControlAPI): hass, config_entry, implementation ) + assert isinstance(implementation, HomePlusControlOAuth2Implementation) + # Create the API authenticated client - external library super().__init__( subscription_key=implementation.subscription_key, diff --git a/mypy.ini b/mypy.ini index 38cb7d4c7f0..f29bcf92720 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2695,12 +2695,6 @@ ignore_errors = true [mypy-homeassistant.components.hassio.websocket_api] ignore_errors = true -[mypy-homeassistant.components.home_plus_control] -ignore_errors = true - -[mypy-homeassistant.components.home_plus_control.api] -ignore_errors = true - [mypy-homeassistant.components.icloud] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d540ed61cd8..f058cfd391f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -40,8 +40,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.hassio.sensor", "homeassistant.components.hassio.system_health", "homeassistant.components.hassio.websocket_api", - "homeassistant.components.home_plus_control", - "homeassistant.components.home_plus_control.api", "homeassistant.components.icloud", "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", From b071affcb4251190884257ba96e7f737920ed5e4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 7 Jul 2022 00:31:47 +0200 Subject: [PATCH 164/820] Use pydeconz interface controls for cover platform (#74535) --- homeassistant/components/deconz/cover.py | 52 ++++++++--- tests/components/deconz/test_cover.py | 105 +++-------------------- 2 files changed, 50 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 9efbeac366f..3e56882f15a 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any, cast +from pydeconz.interfaces.lights import CoverAction from pydeconz.models.event import EventType from pydeconz.models.light.cover import Cover @@ -21,7 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -DEVICE_CLASS = { +DECONZ_TYPE_TO_DEVICE_CLASS = { "Level controllable output": CoverDeviceClass.DAMPER, "Window covering controller": CoverDeviceClass.SHADE, "Window covering device": CoverDeviceClass.SHADE, @@ -40,8 +41,7 @@ async def async_setup_entry( @callback def async_add_cover(_: EventType, cover_id: str) -> None: """Add cover from deCONZ.""" - cover = gateway.api.lights.covers[cover_id] - async_add_entities([DeconzCover(cover, gateway)]) + async_add_entities([DeconzCover(cover_id, gateway)]) gateway.register_platform_add_device_callback( async_add_cover, @@ -55,9 +55,9 @@ class DeconzCover(DeconzDevice, CoverEntity): TYPE = DOMAIN _device: Cover - def __init__(self, device: Cover, gateway: DeconzGateway) -> None: + def __init__(self, cover_id: str, gateway: DeconzGateway) -> None: """Set up cover device.""" - super().__init__(device, gateway) + super().__init__(cover := gateway.api.lights.covers[cover_id], gateway) self._attr_supported_features = CoverEntityFeature.OPEN self._attr_supported_features |= CoverEntityFeature.CLOSE @@ -70,7 +70,7 @@ class DeconzCover(DeconzDevice, CoverEntity): self._attr_supported_features |= CoverEntityFeature.STOP_TILT self._attr_supported_features |= CoverEntityFeature.SET_TILT_POSITION - self._attr_device_class = DEVICE_CLASS.get(self._device.type) + self._attr_device_class = DECONZ_TYPE_TO_DEVICE_CLASS.get(cover.type) @property def current_cover_position(self) -> int: @@ -85,19 +85,31 @@ class DeconzCover(DeconzDevice, CoverEntity): async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = 100 - cast(int, kwargs[ATTR_POSITION]) - await self._device.set_position(lift=position) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + lift=position, + ) async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" - await self._device.open() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.OPEN, + ) async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - await self._device.close() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.CLOSE, + ) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" - await self._device.stop() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.STOP, + ) @property def current_cover_tilt_position(self) -> int | None: @@ -109,16 +121,28 @@ class DeconzCover(DeconzDevice, CoverEntity): async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Tilt the cover to a specific position.""" position = 100 - cast(int, kwargs[ATTR_TILT_POSITION]) - await self._device.set_position(tilt=position) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + tilt=position, + ) async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open cover tilt.""" - await self._device.set_position(tilt=0) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + tilt=0, + ) async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close cover tilt.""" - await self._device.set_position(tilt=100) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + tilt=100, + ) async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop cover tilt.""" - await self._device.stop() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.STOP, + ) diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index c252b00a228..0c37edc221d 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -42,68 +42,48 @@ async def test_cover(hass, aioclient_mock, mock_deconz_websocket): data = { "lights": { "1": { - "name": "Level controllable cover", - "type": "Level controllable output", - "state": {"bri": 254, "on": False, "reachable": True}, - "modelid": "Not zigbee spec", - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - "2": { "name": "Window covering device", "type": "Window covering device", "state": {"lift": 100, "open": False, "reachable": True}, "modelid": "lumi.curtain", "uniqueid": "00:00:00:00:00:00:00:01-00", }, - "3": { + "2": { "name": "Unsupported cover", "type": "Not a cover", "state": {"reachable": True}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, - "4": { - "name": "deconz old brightness cover", - "type": "Level controllable output", - "state": {"bri": 255, "on": False, "reachable": True}, - "modelid": "Not zigbee spec", - "uniqueid": "00:00:00:00:00:00:00:03-00", - }, - "5": { - "name": "Window covering controller", - "type": "Window covering controller", - "state": {"bri": 253, "on": True, "reachable": True}, - "modelid": "Motor controller", - "uniqueid": "00:00:00:00:00:00:00:04-00", - }, } } with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 5 - assert hass.states.get("cover.level_controllable_cover").state == STATE_OPEN - assert hass.states.get("cover.window_covering_device").state == STATE_CLOSED + assert len(hass.states.async_all()) == 2 + cover = hass.states.get("cover.window_covering_device") + assert cover.state == STATE_CLOSED + assert cover.attributes[ATTR_CURRENT_POSITION] == 0 assert not hass.states.get("cover.unsupported_cover") - assert hass.states.get("cover.deconz_old_brightness_cover").state == STATE_OPEN - assert hass.states.get("cover.window_covering_controller").state == STATE_CLOSED - # Event signals cover is closed + # Event signals cover is open event_changed_light = { "t": "event", "e": "changed", "r": "lights", "id": "1", - "state": {"on": True}, + "state": {"lift": 0, "open": True}, } await mock_deconz_websocket(data=event_changed_light) await hass.async_block_till_done() - assert hass.states.get("cover.level_controllable_cover").state == STATE_CLOSED + cover = hass.states.get("cover.window_covering_device") + assert cover.state == STATE_OPEN + assert cover.attributes[ATTR_CURRENT_POSITION] == 100 # Verify service calls for cover - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/2/state") + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") # Service open cover @@ -145,71 +125,10 @@ async def test_cover(hass, aioclient_mock, mock_deconz_websocket): ) assert aioclient_mock.mock_calls[4][2] == {"stop": True} - # Verify service calls for legacy cover - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") - - # Service open cover - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[5][2] == {"on": False} - - # Service close cover - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[6][2] == {"on": True} - - # Service set cover position - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 40}, - blocking=True, - ) - assert aioclient_mock.mock_calls[7][2] == {"bri": 152} - - # Service stop cover movement - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[8][2] == {"bri_inc": 0} - - # Test that a reported cover position of 255 (deconz-rest-api < 2.05.73) is interpreted correctly. - assert hass.states.get("cover.deconz_old_brightness_cover").state == STATE_OPEN - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "4", - "state": {"on": True}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - deconz_old_brightness_cover = hass.states.get("cover.deconz_old_brightness_cover") - assert deconz_old_brightness_cover.state == STATE_CLOSED - assert deconz_old_brightness_cover.attributes[ATTR_CURRENT_POSITION] == 0 - await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 5 + assert len(states) == 2 for state in states: assert state.state == STATE_UNAVAILABLE From 099e7e0637229e127e026ce7288d2d0ce100d29d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Jul 2022 18:20:29 -0500 Subject: [PATCH 165/820] Add oui for tplink es20m (#74526) --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 1 + 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index e9cc687cc02..dfb873564c5 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -10,6 +10,10 @@ "iot_class": "local_polling", "dhcp": [ { "registered_devices": true }, + { + "hostname": "es*", + "macaddress": "54AF97*" + }, { "hostname": "ep*", "macaddress": "E848B8*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index e9cf6ca4c06..57be9a62138 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -128,6 +128,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'tolo', 'hostname': 'usr-tcp232-ed2'}, {'domain': 'toon', 'hostname': 'eneco-*', 'macaddress': '74C63B*'}, {'domain': 'tplink', 'registered_devices': True}, + {'domain': 'tplink', 'hostname': 'es*', 'macaddress': '54AF97*'}, {'domain': 'tplink', 'hostname': 'ep*', 'macaddress': 'E848B8*'}, {'domain': 'tplink', 'hostname': 'ep*', 'macaddress': '003192*'}, {'domain': 'tplink', 'hostname': 'hs*', 'macaddress': '1C3BF3*'}, From fef78939e16d53e3fa2e14e77f56c51d89dec114 Mon Sep 17 00:00:00 2001 From: c-soft Date: Thu, 7 Jul 2022 02:02:08 +0200 Subject: [PATCH 166/820] Bump satel_integra to 0.3.7 to fix compat with python 3.10 (#74543) --- homeassistant/components/satel_integra/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index 6c4a391698b..262507be6bb 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -2,7 +2,7 @@ "domain": "satel_integra", "name": "Satel Integra", "documentation": "https://www.home-assistant.io/integrations/satel_integra", - "requirements": ["satel_integra==0.3.4"], + "requirements": ["satel_integra==0.3.7"], "codeowners": [], "iot_class": "local_push", "loggers": ["satel_integra"] diff --git a/requirements_all.txt b/requirements_all.txt index f4895707f06..17b8c5d7e42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2125,7 +2125,7 @@ samsungctl[websocket]==0.7.1 samsungtvws[async,encrypted]==2.5.0 # homeassistant.components.satel_integra -satel_integra==0.3.4 +satel_integra==0.3.7 # homeassistant.components.dhcp scapy==2.4.5 From 235abb0c10bfe994f1088691472eb07ee4247228 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 7 Jul 2022 00:27:55 +0000 Subject: [PATCH 167/820] [ci skip] Translation update --- .../components/anthemav/translations/id.json | 14 ++++++++++++ .../components/apple_tv/translations/hu.json | 2 +- .../components/awair/translations/id.json | 3 +++ .../components/brunt/translations/hu.json | 2 +- .../components/esphome/translations/hu.json | 4 ++-- .../components/generic/translations/id.json | 2 ++ .../homeassistant/translations/id.json | 1 + .../humidifier/translations/hu.json | 2 +- .../components/isy994/translations/hu.json | 2 +- .../lg_soundbar/translations/id.json | 14 ++++++++++++ .../components/life360/translations/id.json | 19 +++++++++++++++- .../components/nextdns/translations/ca.json | 5 +++++ .../components/nextdns/translations/el.json | 5 +++++ .../components/nextdns/translations/en.json | 5 +++++ .../components/nextdns/translations/fr.json | 5 +++++ .../components/nextdns/translations/hu.json | 5 +++++ .../components/nextdns/translations/id.json | 22 +++++++++++++++++++ .../components/nextdns/translations/pl.json | 5 +++++ .../nextdns/translations/pt-BR.json | 5 +++++ .../nextdns/translations/zh-Hant.json | 5 +++++ .../components/nina/translations/id.json | 17 ++++++++++++++ .../components/rfxtrx/translations/hu.json | 2 +- .../soundtouch/translations/id.json | 16 ++++++++++++++ .../unifiprotect/translations/hu.json | 2 +- 24 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/id.json create mode 100644 homeassistant/components/lg_soundbar/translations/id.json create mode 100644 homeassistant/components/nextdns/translations/id.json create mode 100644 homeassistant/components/soundtouch/translations/id.json diff --git a/homeassistant/components/anthemav/translations/id.json b/homeassistant/components/anthemav/translations/id.json new file mode 100644 index 00000000000..d6bac04f480 --- /dev/null +++ b/homeassistant/components/anthemav/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 4c3a8cdee94..9105c545758 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -41,7 +41,7 @@ "title": "Jelsz\u00f3 sz\u00fcks\u00e9ges" }, "protocol_disabled": { - "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protokoll}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rj\u00fck, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", + "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protocol}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rj\u00fck, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", "title": "A p\u00e1ros\u00edt\u00e1s nem lehets\u00e9ges" }, "reconfigure": { diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 2c6fab90909..ab9431a6a46 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -17,6 +17,9 @@ }, "description": "Masukkan kembali token akses pengembang Awair Anda." }, + "reauth_confirm": { + "description": "Masukkan kembali token akses pengembang Awair Anda." + }, "user": { "data": { "access_token": "Token Akses", diff --git a/homeassistant/components/brunt/translations/hu.json b/homeassistant/components/brunt/translations/hu.json index 3abb5cbf297..bc7be29ba1b 100644 --- a/homeassistant/components/brunt/translations/hu.json +++ b/homeassistant/components/brunt/translations/hu.json @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rem, adja meg \u00fajra a jelsz\u00f3t: {felhaszn\u00e1l\u00f3n\u00e9v}", + "description": "K\u00e9rem, adja meg \u00fajra a jelsz\u00f3t: {username}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index e8e6c9b2dc2..3eaf276b309 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -9,7 +9,7 @@ "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML konfigur\u00e1ci\u00f3 tartalmaz egy \"api:\" sort.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_psk": "Az adat\u00e1tviteli titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen. K\u00e9rj\u00fck, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy megegyezik a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151vel.", - "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rem, \u00e1ll\u00edtson be egy statikus IP-c\u00edmet: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rem, \u00e1ll\u00edtson be egy statikus IP-c\u00edmet." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rem, adja meg az [ESPHome](https://esphomelib.com/) csom\u00f3pontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." + "description": "K\u00e9rem, adja meg az [ESPHome]({esphome_url}) csom\u00f3pontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." } } } diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index a9c553580ca..77ea5f01f0b 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -7,6 +7,7 @@ "error": { "already_exists": "Kamera dengan setelan URL ini sudah ada.", "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", + "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", @@ -50,6 +51,7 @@ "error": { "already_exists": "Kamera dengan setelan URL ini sudah ada.", "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", + "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", diff --git a/homeassistant/components/homeassistant/translations/id.json b/homeassistant/components/homeassistant/translations/id.json index f795a47ee20..7c2994d8bbb 100644 --- a/homeassistant/components/homeassistant/translations/id.json +++ b/homeassistant/components/homeassistant/translations/id.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arsitektur CPU", + "config_dir": "Direktori Konfigurasi", "dev": "Pengembangan", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/humidifier/translations/hu.json b/homeassistant/components/humidifier/translations/hu.json index 6572f0e4955..8c8312dc627 100644 --- a/homeassistant/components/humidifier/translations/hu.json +++ b/homeassistant/components/humidifier/translations/hu.json @@ -14,7 +14,7 @@ }, "trigger_type": { "changed_states": "{entity_name} be- vagy kikapcsolt", - "target_humidity_changed": "{name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott", + "target_humidity_changed": "{entity_name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" } diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index e5a5c4d6f98..4ea107b469f 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -27,7 +27,7 @@ "tls": "Az ISY vez\u00e9rl\u0151 TLS verzi\u00f3ja.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "A c\u00edm bejegyz\u00e9s\u00e9nek teljes URL form\u00e1tumban kell lennie, pl. Http://192.168.10.100:80", + "description": "A c\u00edm bejegyz\u00e9s\u00e9nek teljes URL form\u00e1tumban kell lennie, pl. http://192.168.10.100:80", "title": "Csatlakozzon az ISY-hez" } } diff --git a/homeassistant/components/lg_soundbar/translations/id.json b/homeassistant/components/lg_soundbar/translations/id.json new file mode 100644 index 00000000000..70ce3eb75e9 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/id.json b/homeassistant/components/life360/translations/id.json index 21a93366c44..b219745572d 100644 --- a/homeassistant/components/life360/translations/id.json +++ b/homeassistant/components/life360/translations/id.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Akun sudah dikonfigurasi", "invalid_auth": "Autentikasi tidak valid", + "reauth_successful": "Autentikasi ulang berhasil", "unknown": "Kesalahan yang tidak diharapkan" }, "create_entry": { @@ -9,6 +11,7 @@ }, "error": { "already_configured": "Akun sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "invalid_username": "Nama pengguna tidak valid", "unknown": "Kesalahan yang tidak diharapkan" @@ -20,7 +23,21 @@ "username": "Nama Pengguna" }, "description": "Untuk mengatur opsi tingkat lanjut, baca [dokumentasi Life360]({docs_url}).\nAnda mungkin ingin melakukannya sebelum menambahkan akun.", - "title": "Info Akun Life360" + "title": "Konfigurasikan Akun Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Tampilkan mengemudi sebagai status", + "driving_speed": "Kecepatan mengemudi", + "limit_gps_acc": "Batasi akurasi GPS", + "max_gps_accuracy": "Akurasi GPS maksimum (meter)", + "set_drive_speed": "Tetapkan ambang batas kecepatan mengemudi" + }, + "title": "Opsi Akun" } } } diff --git a/homeassistant/components/nextdns/translations/ca.json b/homeassistant/components/nextdns/translations/ca.json index 83a2c018da5..dd38c4f0b91 100644 --- a/homeassistant/components/nextdns/translations/ca.json +++ b/homeassistant/components/nextdns/translations/ca.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/el.json b/homeassistant/components/nextdns/translations/el.json index 2d5003a1d7a..14c913febcf 100644 --- a/homeassistant/components/nextdns/translations/el.json +++ b/homeassistant/components/nextdns/translations/el.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "\u03a0\u03c1\u03bf\u03c3\u03ad\u03b3\u03b3\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/en.json b/homeassistant/components/nextdns/translations/en.json index c765d53a4d9..d5634514010 100644 --- a/homeassistant/components/nextdns/translations/en.json +++ b/homeassistant/components/nextdns/translations/en.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach server" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/fr.json b/homeassistant/components/nextdns/translations/fr.json index bfebc9078a5..c5f91ef80cc 100644 --- a/homeassistant/components/nextdns/translations/fr.json +++ b/homeassistant/components/nextdns/translations/fr.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Serveur atteint" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/hu.json b/homeassistant/components/nextdns/translations/hu.json index 33b5d858e8a..2491f625b4f 100644 --- a/homeassistant/components/nextdns/translations/hu.json +++ b/homeassistant/components/nextdns/translations/hu.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Szerver el\u00e9r\u00e9se" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/id.json b/homeassistant/components/nextdns/translations/id.json new file mode 100644 index 00000000000..9ebb40ae2d8 --- /dev/null +++ b/homeassistant/components/nextdns/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Profil NextDNS ini sudah dikonfigurasi." + }, + "error": { + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "Kunci API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pl.json b/homeassistant/components/nextdns/translations/pl.json index b7b74af7f1b..f23d4da29d3 100644 --- a/homeassistant/components/nextdns/translations/pl.json +++ b/homeassistant/components/nextdns/translations/pl.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt-BR.json b/homeassistant/components/nextdns/translations/pt-BR.json index 2982a6daad7..90d7cf3f31e 100644 --- a/homeassistant/components/nextdns/translations/pt-BR.json +++ b/homeassistant/components/nextdns/translations/pt-BR.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor de alcance" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/zh-Hant.json b/homeassistant/components/nextdns/translations/zh-Hant.json index 19d2f137a39..5544f29662f 100644 --- a/homeassistant/components/nextdns/translations/zh-Hant.json +++ b/homeassistant/components/nextdns/translations/zh-Hant.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach \u4f3a\u670d\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/id.json b/homeassistant/components/nina/translations/id.json index 03f6feecc22..7e3642a04ef 100644 --- a/homeassistant/components/nina/translations/id.json +++ b/homeassistant/components/nina/translations/id.json @@ -23,5 +23,22 @@ "title": "Pilih kota/kabupaten" } } + }, + "options": { + "error": { + "no_selection": "Pilih setidaknya satu kota/kabupaten" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Kota/kabupaten (A-D)", + "_e_to_h": "Kota/kabupaten (E-H)", + "_i_to_l": "Kota/kabupaten (I-L)", + "_m_to_q": "Kota/kabupaten (M-Q)", + "_r_to_u": "Kota/kabupaten (R-U)" + }, + "title": "Opsi" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 158411ef618..ebdde5c9428 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -45,7 +45,7 @@ "send_status": "\u00c1llapotfriss\u00edt\u00e9s k\u00fcld\u00e9se: {subtype}" }, "trigger_type": { - "command": "Be\u00e9rkezett parancs: {alt\u00edpus}", + "command": "Be\u00e9rkezett parancs: {subtype}", "status": "Be\u00e9rkezett st\u00e1tusz: {subtype}" } }, diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json new file mode 100644 index 00000000000..71b7e84f1e2 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "zeroconf_confirm": { + "description": "Anda akan menambahkan perangkat SoundTouch bernama `{name}` ke Home Assistant.", + "title": "Konfirmasi penambahan perangkat Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index 11e0151f165..cbef293c7e9 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -16,7 +16,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ipaddress})? Egy helyi felhaszn\u00e1l\u00f3t kell l\u00e9trehoznia Unifi OS-ben.", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ipaddress})? A bejelentkez\u00e9shez egy helyi felhaszn\u00e1l\u00f3ra lesz sz\u00fcks\u00e9g, amelyet az UniFi OS Console-ban hoztak l\u00e9tre. Az Ubiquiti Cloud Users nem fog m\u0171k\u00f6dni. Tov\u00e1bbi inform\u00e1ci\u00f3: {local_user_documentation_url}", "title": "UniFi Protect felfedezve" }, "reauth_confirm": { From 104d236646b1a15d39c4eb80512ea7b0fa6c9dda Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 7 Jul 2022 08:40:10 +0200 Subject: [PATCH 168/820] fjaraskupan: Make sure we stop bleak on home assistant stop (#74545) * Make sure we stop bleak on home assistant stop * Fix typing Co-authored-by: Martin Hjelmare --- homeassistant/components/fjaraskupan/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 4c4f19403a6..85e95db5513 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -12,8 +12,8 @@ from bleak.backends.scanner import AdvertisementData from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -131,6 +131,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner.register_detection_callback(detection_callback) await scanner.start() + async def on_hass_stop(event: Event) -> None: + await scanner.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True From 4a4dabaaa5769ee69b12b0f808a09b3423603596 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Jul 2022 09:03:43 +0200 Subject: [PATCH 169/820] Fix openweathermap hourly forecast (#74578) --- homeassistant/components/openweathermap/weather.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index ea439a35586..9948ecc0428 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -143,7 +143,11 @@ class OpenWeatherMapWeather(WeatherEntity): """Return the forecast array.""" api_forecasts = self._weather_coordinator.data[ATTR_API_FORECAST] forecasts = [ - {ha_key: forecast[api_key] for api_key, ha_key in FORECAST_MAP.items()} + { + ha_key: forecast[api_key] + for api_key, ha_key in FORECAST_MAP.items() + if api_key in forecast + } for forecast in api_forecasts ] return cast(list[Forecast], forecasts) From 46f2abc38c7776ce2dc0c583ffe90d0172cc691b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 09:22:35 +0200 Subject: [PATCH 170/820] Use generics in NextDNS (#74517) Use generics in nextdns --- homeassistant/components/nextdns/__init__.py | 17 ++++--- homeassistant/components/nextdns/sensor.py | 49 ++++++++++++++++---- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index f9cb1f60cd1..7e7f5ff2dd8 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging +from typing import TypeVar from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout @@ -39,8 +40,10 @@ from .const import ( UPDATE_INTERVAL_ANALYTICS, ) +TCoordinatorData = TypeVar("TCoordinatorData", bound=NextDnsData) -class NextDnsUpdateCoordinator(DataUpdateCoordinator): + +class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): """Class to manage fetching NextDNS data API.""" def __init__( @@ -64,12 +67,12 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - async def _async_update_data(self) -> NextDnsData: + async def _async_update_data(self) -> TCoordinatorData: """Update data via library.""" raise NotImplementedError("Update method not implemented") -class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsStatus]): """Class to manage fetching NextDNS analytics status data from API.""" async def _async_update_data(self) -> AnalyticsStatus: @@ -81,7 +84,7 @@ class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator): raise UpdateFailed(err) from err -class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsDnssec]): """Class to manage fetching NextDNS analytics Dnssec data from API.""" async def _async_update_data(self) -> AnalyticsDnssec: @@ -93,7 +96,7 @@ class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator): raise UpdateFailed(err) from err -class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsEncryption]): """Class to manage fetching NextDNS analytics encryption data from API.""" async def _async_update_data(self) -> AnalyticsEncryption: @@ -105,7 +108,7 @@ class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator): raise UpdateFailed(err) from err -class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsIpVersions]): """Class to manage fetching NextDNS analytics IP versions data from API.""" async def _async_update_data(self) -> AnalyticsIpVersions: @@ -117,7 +120,7 @@ class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator): raise UpdateFailed(err) from err -class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsProtocols]): """Class to manage fetching NextDNS analytics protocols data from API.""" async def _async_update_data(self) -> AnalyticsProtocols: diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index ef0176b071a..71d71ff287b 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -3,7 +3,15 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, cast +from typing import Generic, cast + +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, +) from homeassistant.components.sensor import ( SensorEntity, @@ -18,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NextDnsUpdateCoordinator +from . import NextDnsUpdateCoordinator, TCoordinatorData from .const import ( ATTR_DNSSEC, ATTR_ENCRYPTION, @@ -32,23 +40,26 @@ PARALLEL_UPDATES = 1 @dataclass -class NextDnsSensorRequiredKeysMixin: +class NextDnsSensorRequiredKeysMixin(Generic[TCoordinatorData]): """Class for NextDNS entity required keys.""" + coordinator_class: type[TCoordinatorData] coordinator_type: str - value: Callable[[Any], StateType] + value: Callable[[TCoordinatorData], StateType] @dataclass class NextDnsSensorEntityDescription( - SensorEntityDescription, NextDnsSensorRequiredKeysMixin + SensorEntityDescription, + NextDnsSensorRequiredKeysMixin[TCoordinatorData], ): """NextDNS sensor entity description.""" -SENSORS = ( +SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( NextDnsSensorEntityDescription( key="all_queries", + coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -59,6 +70,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="blocked_queries", + coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -69,6 +81,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="relayed_queries", + coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -79,6 +92,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="blocked_queries_ratio", + coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -89,6 +103,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="doh_queries", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -100,6 +115,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="dot_queries", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -111,6 +127,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="doq_queries", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -122,6 +139,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="udp_queries", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -133,6 +151,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="doh_queries_ratio", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_registry_enabled_default=False, icon="mdi:dns", @@ -144,6 +163,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="dot_queries_ratio", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -155,6 +175,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="doq_queries_ratio", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_registry_enabled_default=False, icon="mdi:dns", @@ -166,6 +187,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="udp_queries_ratio", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -177,6 +199,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="encrypted_queries", + coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -188,6 +211,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="unencrypted_queries", + coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -199,6 +223,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="encrypted_queries_ratio", + coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -210,6 +235,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="ipv4_queries", + coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -221,6 +247,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="ipv6_queries", + coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -232,6 +259,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="ipv6_queries_ratio", + coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -243,6 +271,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="validated_queries", + coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -254,6 +283,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="not_validated_queries", + coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -265,6 +295,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="validated_queries_ratio", + coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -294,12 +325,14 @@ async def async_setup_entry( async_add_entities(sensors) -class NextDnsSensor(CoordinatorEntity[NextDnsUpdateCoordinator], SensorEntity): +class NextDnsSensor( + CoordinatorEntity[NextDnsUpdateCoordinator[TCoordinatorData]], SensorEntity +): """Define an NextDNS sensor.""" def __init__( self, - coordinator: NextDnsUpdateCoordinator, + coordinator: NextDnsUpdateCoordinator[TCoordinatorData], description: NextDnsSensorEntityDescription, ) -> None: """Initialize.""" From e56357b4f2efb0b979238c4096bfca09437fde54 Mon Sep 17 00:00:00 2001 From: ufodone <35497351+ufodone@users.noreply.github.com> Date: Thu, 7 Jul 2022 00:33:32 -0700 Subject: [PATCH 171/820] Bump pyenvisalink version to 4.6 (#74561) --- homeassistant/components/envisalink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 44a40991a37..8d0fc735b90 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -2,7 +2,7 @@ "domain": "envisalink", "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", - "requirements": ["pyenvisalink==4.5"], + "requirements": ["pyenvisalink==4.6"], "codeowners": ["@ufodone"], "iot_class": "local_push", "loggers": ["pyenvisalink"] diff --git a/requirements_all.txt b/requirements_all.txt index 17b8c5d7e42..90742fd4875 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1477,7 +1477,7 @@ pyeight==0.3.0 pyemby==1.8 # homeassistant.components.envisalink -pyenvisalink==4.5 +pyenvisalink==4.6 # homeassistant.components.ephember pyephember==0.3.1 From 681735b94cfc5ff1668c0eb54995f25ebde62d2b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 7 Jul 2022 03:39:37 -0400 Subject: [PATCH 172/820] Bump aioskybell to 22.7.0 (#74559) --- homeassistant/components/skybell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index c0e66aa5462..bfef4bc3422 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -3,7 +3,7 @@ "name": "SkyBell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["aioskybell==22.6.1"], + "requirements": ["aioskybell==22.7.0"], "dependencies": ["ffmpeg"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 90742fd4875..97d62c96420 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.6.1 +aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5c616edf2e..06dcf27cbe5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -216,7 +216,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.6.1 +aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 From d203cb0658f3729c75d3966306d3931a9fc763c8 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 7 Jul 2022 02:43:10 -0500 Subject: [PATCH 173/820] Minimize Sonos `media_player.unjoin` timeout (#74549) --- homeassistant/components/sonos/media_player.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index c68110d9763..470386c341d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -69,6 +69,7 @@ from .speaker import SonosMedia, SonosSpeaker _LOGGER = logging.getLogger(__name__) LONG_SERVICE_TIMEOUT = 30.0 +UNJOIN_SERVICE_TIMEOUT = 0.1 VOLUME_INCREMENT = 2 REPEAT_TO_SONOS = { @@ -775,7 +776,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_unjoin_player(self): """Remove this player from any group. - Coalesces all calls within 0.5s to allow use of SonosSpeaker.unjoin_multi() + Coalesces all calls within UNJOIN_SERVICE_TIMEOUT to allow use of SonosSpeaker.unjoin_multi() which optimizes the order in which speakers are removed from their groups. Removing coordinators last better preserves playqueues on the speakers. """ @@ -785,6 +786,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_process_unjoin(now: datetime.datetime) -> None: """Process the unjoin with all remove requests within the coalescing period.""" unjoin_data = sonos_data.unjoin_data.pop(household_id) + _LOGGER.debug( + "Processing unjoins for %s", [x.zone_name for x in unjoin_data.speakers] + ) await SonosSpeaker.unjoin_multi(self.hass, unjoin_data.speakers) unjoin_data.event.set() @@ -794,6 +798,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): unjoin_data = sonos_data.unjoin_data[household_id] = UnjoinData( speakers=[self.speaker] ) - async_call_later(self.hass, 0.5, async_process_unjoin) + async_call_later(self.hass, UNJOIN_SERVICE_TIMEOUT, async_process_unjoin) + _LOGGER.debug("Requesting unjoin for %s", self.speaker.zone_name) await unjoin_data.event.wait() From ae295f1bf5fc9564533747e04ee22c624f905308 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 7 Jul 2022 04:49:32 -0400 Subject: [PATCH 174/820] Add three decimal places of sub-second resolution to root logger timestamps (#74518) --- homeassistant/bootstrap.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 81d0fa72134..939d3073f57 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -285,7 +285,9 @@ def async_enable_logging( This method must be run in the event loop. """ - fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" + fmt = ( + "%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) [%(name)s] %(message)s" + ) datefmt = "%Y-%m-%d %H:%M:%S" if not log_no_color: From 0c29b68cf82c777ec6fd70ce38911f1d1e39d26a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jul 2022 03:57:44 -0500 Subject: [PATCH 175/820] Switch linear search to a dict lookup for ip bans (#74482) --- homeassistant/components/http/ban.py | 101 ++++++++++++---------- tests/components/conftest.py | 3 +- tests/components/http/test_ban.py | 124 ++++++++++++++++++++++++--- 3 files changed, 170 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 620bdc7613c..81349fe95a1 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -26,7 +26,7 @@ from .view import HomeAssistantView _LOGGER: Final = logging.getLogger(__name__) -KEY_BANNED_IPS: Final = "ha_banned_ips" +KEY_BAN_MANAGER: Final = "ha_banned_ips_manager" KEY_FAILED_LOGIN_ATTEMPTS: Final = "ha_failed_login_attempts" KEY_LOGIN_THRESHOLD: Final = "ha_login_threshold" @@ -50,9 +50,9 @@ def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> N async def ban_startup(app: Application) -> None: """Initialize bans when app starts up.""" - app[KEY_BANNED_IPS] = await async_load_ip_bans_config( - hass, hass.config.path(IP_BANS_FILE) - ) + ban_manager = IpBanManager(hass) + await ban_manager.async_load() + app[KEY_BAN_MANAGER] = ban_manager app.on_startup.append(ban_startup) @@ -62,18 +62,17 @@ async def ban_middleware( request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] ) -> StreamResponse: """IP Ban middleware.""" - if KEY_BANNED_IPS not in request.app: + ban_manager: IpBanManager | None = request.app.get(KEY_BAN_MANAGER) + if ban_manager is None: _LOGGER.error("IP Ban middleware loaded but banned IPs not loaded") return await handler(request) - # Verify if IP is not banned - ip_address_ = ip_address(request.remote) # type: ignore[arg-type] - is_banned = any( - ip_ban.ip_address == ip_address_ for ip_ban in request.app[KEY_BANNED_IPS] - ) - - if is_banned: - raise HTTPForbidden() + ip_bans_lookup = ban_manager.ip_bans_lookup + if ip_bans_lookup: + # Verify if IP is not banned + ip_address_ = ip_address(request.remote) # type: ignore[arg-type] + if ip_address_ in ip_bans_lookup: + raise HTTPForbidden() try: return await handler(request) @@ -129,7 +128,7 @@ async def process_wrong_login(request: Request) -> None: ) # Check if ban middleware is loaded - if KEY_BANNED_IPS not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: + if KEY_BAN_MANAGER not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: return request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1 @@ -146,14 +145,9 @@ async def process_wrong_login(request: Request) -> None: request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >= request.app[KEY_LOGIN_THRESHOLD] ): - new_ban = IpBan(remote_addr) - request.app[KEY_BANNED_IPS].append(new_ban) - - await hass.async_add_executor_job( - update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban - ) - + ban_manager: IpBanManager = request.app[KEY_BAN_MANAGER] _LOGGER.warning("Banned IP %s for too many login attempts", remote_addr) + await ban_manager.async_add_ban(remote_addr) persistent_notification.async_create( hass, @@ -173,7 +167,7 @@ async def process_success_login(request: Request) -> None: remote_addr = ip_address(request.remote) # type: ignore[arg-type] # Check if ban middleware is loaded - if KEY_BANNED_IPS not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: + if KEY_BAN_MANAGER not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: return if ( @@ -199,32 +193,49 @@ class IpBan: self.banned_at = banned_at or dt_util.utcnow() -async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> list[IpBan]: - """Load list of banned IPs from config file.""" - ip_list: list[IpBan] = [] +class IpBanManager: + """Manage IP bans.""" - try: - list_ = await hass.async_add_executor_job(load_yaml_config_file, path) - except FileNotFoundError: - return ip_list - except HomeAssistantError as err: - _LOGGER.error("Unable to load %s: %s", path, str(err)) - return ip_list + def __init__(self, hass: HomeAssistant) -> None: + """Init the ban manager.""" + self.hass = hass + self.path = hass.config.path(IP_BANS_FILE) + self.ip_bans_lookup: dict[IPv4Address | IPv6Address, IpBan] = {} - for ip_ban, ip_info in list_.items(): + async def async_load(self) -> None: + """Load the existing IP bans.""" try: - ip_info = SCHEMA_IP_BAN_ENTRY(ip_info) - ip_list.append(IpBan(ip_ban, ip_info["banned_at"])) - except vol.Invalid as err: - _LOGGER.error("Failed to load IP ban %s: %s", ip_info, err) - continue + list_ = await self.hass.async_add_executor_job( + load_yaml_config_file, self.path + ) + except FileNotFoundError: + return + except HomeAssistantError as err: + _LOGGER.error("Unable to load %s: %s", self.path, str(err)) + return - return ip_list + ip_bans_lookup: dict[IPv4Address | IPv6Address, IpBan] = {} + for ip_ban, ip_info in list_.items(): + try: + ip_info = SCHEMA_IP_BAN_ENTRY(ip_info) + ban = IpBan(ip_ban, ip_info["banned_at"]) + ip_bans_lookup[ban.ip_address] = ban + except vol.Invalid as err: + _LOGGER.error("Failed to load IP ban %s: %s", ip_info, err) + continue + self.ip_bans_lookup = ip_bans_lookup -def update_ip_bans_config(path: str, ip_ban: IpBan) -> None: - """Update config file with new banned IP address.""" - with open(path, "a", encoding="utf8") as out: - ip_ = {str(ip_ban.ip_address): {ATTR_BANNED_AT: ip_ban.banned_at.isoformat()}} - out.write("\n") - out.write(yaml.dump(ip_)) + def _add_ban(self, ip_ban: IpBan) -> None: + """Update config file with new banned IP address.""" + with open(self.path, "a", encoding="utf8") as out: + ip_ = { + str(ip_ban.ip_address): {ATTR_BANNED_AT: ip_ban.banned_at.isoformat()} + } + # Write in a single write call to avoid interleaved writes + out.write("\n" + yaml.dump(ip_)) + + async def async_add_ban(self, remote_addr: IPv4Address | IPv6Address) -> None: + """Add a new IP address to the banned list.""" + new_ban = self.ip_bans_lookup[remote_addr] = IpBan(remote_addr) + await self.hass.async_add_executor_job(self._add_ban, new_ban) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index f153263cbc6..6cad53aea72 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -19,8 +19,7 @@ def patch_zeroconf_multiple_catcher(): def prevent_io(): """Fixture to prevent certain I/O from happening.""" with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", - return_value=[], + "homeassistant.components.http.ban.load_yaml_config_file", ): yield diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 5e482d16248..05a6493c9c2 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -15,12 +15,13 @@ import homeassistant.components.http as http from homeassistant.components.http import KEY_AUTHENTICATED from homeassistant.components.http.ban import ( IP_BANS_FILE, - KEY_BANNED_IPS, + KEY_BAN_MANAGER, KEY_FAILED_LOGIN_ATTEMPTS, - IpBan, + IpBanManager, setup_bans, ) from homeassistant.components.http.view import request_handler_factory +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from . import mock_real_ip @@ -58,8 +59,10 @@ async def test_access_from_banned_ip(hass, aiohttp_client): set_real_ip = mock_real_ip(app) with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", - return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS], + "homeassistant.components.http.ban.load_yaml_config_file", + return_value={ + banned_ip: {"banned_at": "2016-11-16T19:20:03"} for banned_ip in BANNED_IPS + }, ): client = await aiohttp_client(app) @@ -69,6 +72,99 @@ async def test_access_from_banned_ip(hass, aiohttp_client): assert resp.status == HTTPStatus.FORBIDDEN +async def test_access_from_banned_ip_with_partially_broken_yaml_file( + hass, aiohttp_client, caplog +): + """Test accessing to server from banned IP. Both trusted and not. + + We inject some garbage into the yaml file to make sure it can + still load the bans. + """ + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + data = {banned_ip: {"banned_at": "2016-11-16T19:20:03"} for banned_ip in BANNED_IPS} + data["5.3.3.3"] = {"banned_at": "garbage"} + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + return_value=data, + ): + client = await aiohttp_client(app) + + for remote_addr in BANNED_IPS: + set_real_ip(remote_addr) + resp = await client.get("/") + assert resp.status == HTTPStatus.FORBIDDEN + + # Ensure garbage data is ignored + set_real_ip("5.3.3.3") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + + assert "Failed to load IP ban" in caplog.text + + +async def test_no_ip_bans_file(hass, aiohttp_client): + """Test no ip bans file.""" + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + side_effect=FileNotFoundError, + ): + client = await aiohttp_client(app) + + set_real_ip("4.3.2.1") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + + +async def test_failure_loading_ip_bans_file(hass, aiohttp_client): + """Test failure loading ip bans file.""" + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + side_effect=HomeAssistantError, + ): + client = await aiohttp_client(app) + + set_real_ip("4.3.2.1") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + + +async def test_ip_ban_manager_never_started(hass, aiohttp_client, caplog): + """Test we handle the ip ban manager not being started.""" + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + side_effect=FileNotFoundError, + ): + client = await aiohttp_client(app) + + # Mock the manager never being started + del app[KEY_BAN_MANAGER] + + set_real_ip("4.3.2.1") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + assert "IP Ban middleware loaded but banned IPs not loaded" in caplog.text + + @pytest.mark.parametrize( "remote_addr, bans, status", list( @@ -95,10 +191,13 @@ async def test_access_from_supervisor_ip( mock_real_ip(app)(remote_addr) with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", return_value=[] + "homeassistant.components.http.ban.load_yaml_config_file", + return_value={}, ): client = await aiohttp_client(app) + manager: IpBanManager = app[KEY_BAN_MANAGER] + assert await async_setup_component(hass, "hassio", {"hassio": {}}) m_open = mock_open() @@ -108,13 +207,13 @@ async def test_access_from_supervisor_ip( ): resp = await client.get("/") assert resp.status == HTTPStatus.UNAUTHORIZED - assert len(app[KEY_BANNED_IPS]) == bans + assert len(manager.ip_bans_lookup) == bans assert m_open.call_count == bans # second request should be forbidden if banned resp = await client.get("/") assert resp.status == status - assert len(app[KEY_BANNED_IPS]) == bans + assert len(manager.ip_bans_lookup) == bans async def test_ban_middleware_not_loaded_by_config(hass): @@ -149,22 +248,25 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): mock_real_ip(app)("200.201.202.204") with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", - return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS], + "homeassistant.components.http.ban.load_yaml_config_file", + return_value={ + banned_ip: {"banned_at": "2016-11-16T19:20:03"} for banned_ip in BANNED_IPS + }, ): client = await aiohttp_client(app) + manager: IpBanManager = app[KEY_BAN_MANAGER] m_open = mock_open() with patch("homeassistant.components.http.ban.open", m_open, create=True): resp = await client.get("/") assert resp.status == HTTPStatus.UNAUTHORIZED - assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) + assert len(manager.ip_bans_lookup) == len(BANNED_IPS) assert m_open.call_count == 0 resp = await client.get("/") assert resp.status == HTTPStatus.UNAUTHORIZED - assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) + 1 + assert len(manager.ip_bans_lookup) == len(BANNED_IPS) + 1 m_open.assert_called_once_with( hass.config.path(IP_BANS_FILE), "a", encoding="utf8" ) From 42615950788569d72a93a730fcac26447c950601 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 11:00:34 +0200 Subject: [PATCH 176/820] Update orjson to 3.7.7 (#74581) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8795e570216..d72cb6445a5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.7 -orjson==3.7.5 +orjson==3.7.7 paho-mqtt==1.6.1 pillow==9.2.0 pip>=21.0,<22.2 diff --git a/pyproject.toml b/pyproject.toml index 59ad04c4b11..60e0865fab9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", - "orjson==3.7.5", + "orjson==3.7.7", "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index a8ec77333e0..7826a9a0f26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jinja2==3.1.2 lru-dict==1.1.7 PyJWT==2.4.0 cryptography==36.0.2 -orjson==3.7.5 +orjson==3.7.7 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 From 5ae593672e5ef2949560921c289905fbba208350 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 11:35:45 +0200 Subject: [PATCH 177/820] Remove google_assistant from mypy ignore list (#74587) --- homeassistant/components/google_assistant/helpers.py | 2 +- homeassistant/components/google_assistant/http.py | 2 +- .../components/google_assistant/report_state.py | 3 ++- homeassistant/components/google_assistant/trait.py | 10 ++++++---- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 6f81ddebdb4..0b9b12f2f4c 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -51,7 +51,7 @@ LOCAL_SDK_MIN_VERSION = AwesomeVersion("2.1.5") @callback def _get_registry_entries( hass: HomeAssistant, entity_id: str -) -> tuple[device_registry.DeviceEntry, area_registry.AreaEntry]: +) -> tuple[device_registry.DeviceEntry | None, area_registry.AreaEntry | None]: """Get registry entries.""" ent_reg = entity_registry.async_get(hass) dev_reg = device_registry.async_get(hass) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 3f3db1f2b5b..84d5e4a3364 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -59,7 +59,7 @@ def _get_homegraph_jwt(time, iss, key): async def _get_homegraph_token( hass: HomeAssistant, jwt_signed: str -) -> dict[str, Any] | list[str, Any] | Any: +) -> dict[str, Any] | list[Any] | Any: headers = { "Authorization": f"Bearer {jwt_signed}", "Content-Type": "application/x-www-form-urlencoded", diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 4e8ac1624cc..737b54c8b1e 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import deque import logging +from typing import Any from homeassistant.const import MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback @@ -28,7 +29,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig """Enable state reporting.""" checker = None unsub_pending: CALLBACK_TYPE | None = None - pending = deque([{}]) + pending: deque[dict[str, Any]] = deque([{}]) async def report_states(now=None): """Report the states.""" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 42fc43197ea..ee41dd0c678 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -176,6 +176,8 @@ def _next_selected(items: list[str], selected: str | None) -> str | None: If selected is missing in items, None is returned """ + if selected is None: + return None try: index = items.index(selected) except ValueError: @@ -188,7 +190,7 @@ def _next_selected(items: list[str], selected: str | None) -> str | None: class _Trait: """Represents a Trait inside Google Assistant skill.""" - commands = [] + commands: list[str] = [] @staticmethod def might_2fa(domain, features, device_class): @@ -1701,7 +1703,7 @@ class InputSelectorTrait(_Trait): name = TRAIT_INPUTSELECTOR commands = [COMMAND_INPUT, COMMAND_NEXT_INPUT, COMMAND_PREVIOUS_INPUT] - SYNONYMS = {} + SYNONYMS: dict[str, list[str]] = {} @staticmethod def supported(domain, features, device_class, _): @@ -2197,7 +2199,7 @@ class MediaStateTrait(_Trait): """ name = TRAIT_MEDIA_STATE - commands = [] + commands: list[str] = [] activity_lookup = { STATE_OFF: "INACTIVE", @@ -2314,7 +2316,7 @@ class SensorStateTrait(_Trait): } name = TRAIT_SENSOR_STATE - commands = [] + commands: list[str] = [] @classmethod def supported(cls, domain, features, device_class, _): diff --git a/mypy.ini b/mypy.ini index f29bcf92720..5c5675bcb37 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2662,18 +2662,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.water_heater] ignore_errors = true -[mypy-homeassistant.components.google_assistant.helpers] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.http] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.report_state] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.trait] -ignore_errors = true - [mypy-homeassistant.components.hassio] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f058cfd391f..a1955c51993 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -29,10 +29,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.evohome", "homeassistant.components.evohome.climate", "homeassistant.components.evohome.water_heater", - "homeassistant.components.google_assistant.helpers", - "homeassistant.components.google_assistant.http", - "homeassistant.components.google_assistant.report_state", - "homeassistant.components.google_assistant.trait", "homeassistant.components.hassio", "homeassistant.components.hassio.auth", "homeassistant.components.hassio.binary_sensor", From 4604694255701b591e484c2573d722b955cde8f3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 11:40:17 +0200 Subject: [PATCH 178/820] Add deprecation to PR template (#74583) --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 92de30ffe5a..52d25226930 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -33,6 +33,7 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New integration (thank you!) - [ ] New feature (which adds functionality to an existing integration) +- [ ] Deprecation (breaking change to happen in the future) - [ ] Breaking change (fix/feature causing existing functionality to break) - [ ] Code quality improvements to existing code or addition of tests From f19c542d6d470f2ba1f4e41c3b47b535ea5e5fd7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 12:14:46 +0200 Subject: [PATCH 179/820] Remove denonavr from mypy ignore list (#74580) --- .../components/denonavr/config_flow.py | 15 +- .../components/denonavr/media_player.py | 152 ++++++++++-------- homeassistant/components/denonavr/receiver.py | 7 +- mypy.ini | 9 -- script/hassfest/mypy_config.py | 3 - 5 files changed, 94 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index fe6c05b3aca..2ec58df8126 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -90,14 +90,14 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Denon AVR flow.""" - self.host = None - self.serial_number = None - self.model_name = None + self.host: str | None = None + self.serial_number: str | None = None + self.model_name: str | None = None self.timeout = DEFAULT_TIMEOUT self.show_all_sources = DEFAULT_SHOW_SOURCES self.zone2 = DEFAULT_ZONE2 self.zone3 = DEFAULT_ZONE3 - self.d_receivers = [] + self.d_receivers: list[dict[str, Any]] = [] @staticmethod @callback @@ -138,7 +138,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle multiple receivers found.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: self.host = user_input["select_host"] return await self.async_step_connect() @@ -169,6 +169,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Connect to the receiver.""" + assert self.host connect_denonavr = ConnectDenonAVR( self.host, self.timeout, @@ -185,6 +186,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not success: return self.async_abort(reason="cannot_connect") receiver = connect_denonavr.receiver + assert receiver if not self.serial_number: self.serial_number = receiver.serial_number @@ -238,6 +240,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "*", "" ) self.serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + assert discovery_info.ssdp_location is not None self.host = urlparse(discovery_info.ssdp_location).hostname if self.model_name in IGNORED_MODELS: @@ -260,6 +263,6 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() @staticmethod - def construct_unique_id(model_name: str, serial_number: str) -> str: + def construct_unique_id(model_name: str | None, serial_number: str | None) -> str: """Construct the unique id from the ssdp discovery or user_step.""" return f"{model_name}-{serial_number}" diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 85e28c29d7c..10c3cb4f6b0 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -1,10 +1,11 @@ """Support for Denon AVR receivers using their HTTP interface.""" from __future__ import annotations -from collections.abc import Coroutine +from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta from functools import wraps import logging +from typing import Any, TypeVar from denonavr import DenonAVR from denonavr.const import POWER_ON @@ -15,6 +16,7 @@ from denonavr.exceptions import ( AvrTimoutError, DenonAvrError, ) +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components.media_player import ( @@ -79,6 +81,10 @@ SERVICE_GET_COMMAND = "get_command" SERVICE_SET_DYNAMIC_EQ = "set_dynamic_eq" SERVICE_UPDATE_AUDYSSEY = "update_audyssey" +_DenonDeviceT = TypeVar("_DenonDeviceT", bound="DenonDevice") +_R = TypeVar("_R") +_P = ParamSpec("_P") + async def async_setup_entry( hass: HomeAssistant, @@ -131,6 +137,79 @@ async def async_setup_entry( async_add_entities(entities, update_before_add=True) +def async_log_errors( + func: Callable[Concatenate[_DenonDeviceT, _P], Awaitable[_R]], +) -> Callable[Concatenate[_DenonDeviceT, _P], Coroutine[Any, Any, _R | None]]: + """ + Log errors occurred when calling a Denon AVR receiver. + + Decorates methods of DenonDevice class. + Declaration of staticmethod for this method is at the end of this class. + """ + + @wraps(func) + async def wrapper( + self: _DenonDeviceT, *args: _P.args, **kwargs: _P.kwargs + ) -> _R | None: + # pylint: disable=protected-access + available = True + try: + return await func(self, *args, **kwargs) + except AvrTimoutError: + available = False + if self._available is True: + _LOGGER.warning( + "Timeout connecting to Denon AVR receiver at host %s. " + "Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrNetworkError: + available = False + if self._available is True: + _LOGGER.warning( + "Network error connecting to Denon AVR receiver at host %s. " + "Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrForbiddenError: + available = False + if self._available is True: + _LOGGER.warning( + "Denon AVR receiver at host %s responded with HTTP 403 error. " + "Device is unavailable. Please consider power cycling your " + "receiver", + self._receiver.host, + ) + self._available = False + except AvrCommandError as err: + available = False + _LOGGER.error( + "Command %s failed with error: %s", + func.__name__, + err, + ) + except DenonAvrError as err: + available = False + _LOGGER.error( + "Error %s occurred in method %s for Denon AVR receiver", + err, + func.__name__, + exc_info=True, + ) + finally: + if available is True and self._available is False: + _LOGGER.info( + "Denon AVR receiver at host %s is available again", + self._receiver.host, + ) + self._available = True + return None + + return wrapper + + class DenonDevice(MediaPlayerEntity): """Representation of a Denon Media Player Device.""" @@ -144,6 +223,7 @@ class DenonDevice(MediaPlayerEntity): """Initialize the device.""" self._attr_name = receiver.name self._attr_unique_id = unique_id + assert config_entry.unique_id self._attr_device_info = DeviceInfo( configuration_url=f"http://{config_entry.data[CONF_HOST]}/", identifiers={(DOMAIN, config_entry.unique_id)}, @@ -163,71 +243,6 @@ class DenonDevice(MediaPlayerEntity): ) self._available = True - def async_log_errors( - func: Coroutine, - ) -> Coroutine: - """ - Log errors occurred when calling a Denon AVR receiver. - - Decorates methods of DenonDevice class. - Declaration of staticmethod for this method is at the end of this class. - """ - - @wraps(func) - async def wrapper(self, *args, **kwargs): - # pylint: disable=protected-access - available = True - try: - return await func(self, *args, **kwargs) - except AvrTimoutError: - available = False - if self._available is True: - _LOGGER.warning( - "Timeout connecting to Denon AVR receiver at host %s. Device is unavailable", - self._receiver.host, - ) - self._available = False - except AvrNetworkError: - available = False - if self._available is True: - _LOGGER.warning( - "Network error connecting to Denon AVR receiver at host %s. Device is unavailable", - self._receiver.host, - ) - self._available = False - except AvrForbiddenError: - available = False - if self._available is True: - _LOGGER.warning( - "Denon AVR receiver at host %s responded with HTTP 403 error. Device is unavailable. Please consider power cycling your receiver", - self._receiver.host, - ) - self._available = False - except AvrCommandError as err: - available = False - _LOGGER.error( - "Command %s failed with error: %s", - func.__name__, - err, - ) - except DenonAvrError as err: - available = False - _LOGGER.error( - "Error %s occurred in method %s for Denon AVR receiver", - err, - func.__name__, - exc_info=True, - ) - finally: - if available is True and self._available is False: - _LOGGER.info( - "Denon AVR receiver at host %s is available again", - self._receiver.host, - ) - self._available = True - - return wrapper - @async_log_errors async def async_update(self) -> None: """Get the latest status information from device.""" @@ -466,8 +481,3 @@ class DenonDevice(MediaPlayerEntity): if self._update_audyssey: await self._receiver.async_update_audyssey() - - # Decorator defined before is a staticmethod - async_log_errors = staticmethod( # pylint: disable=no-staticmethod-decorator - async_log_errors - ) diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index 5c15468e6d4..28969d25792 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -23,12 +23,12 @@ class ConnectDenonAVR: ) -> None: """Initialize the class.""" self._async_client_getter = async_client_getter - self._receiver = None + self._receiver: DenonAVR | None = None self._host = host self._show_all_inputs = show_all_inputs self._timeout = timeout - self._zones = {} + self._zones: dict[str, str | None] = {} if zone2: self._zones["Zone2"] = None if zone3: @@ -42,6 +42,7 @@ class ConnectDenonAVR: async def async_connect_receiver(self) -> bool: """Connect to the DenonAVR receiver.""" await self.async_init_receiver_class() + assert self._receiver if ( self._receiver.manufacturer is None @@ -70,7 +71,7 @@ class ConnectDenonAVR: return True - async def async_init_receiver_class(self) -> bool: + async def async_init_receiver_class(self) -> None: """Initialize the DenonAVR class asynchronously.""" receiver = DenonAVR( host=self._host, diff --git a/mypy.ini b/mypy.ini index 5c5675bcb37..c4a135322fe 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2644,15 +2644,6 @@ ignore_errors = true [mypy-homeassistant.components.conversation.default_agent] ignore_errors = true -[mypy-homeassistant.components.denonavr.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.denonavr.media_player] -ignore_errors = true - -[mypy-homeassistant.components.denonavr.receiver] -ignore_errors = true - [mypy-homeassistant.components.evohome] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a1955c51993..e7c5e0ed129 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -23,9 +23,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.http_api", "homeassistant.components.conversation", "homeassistant.components.conversation.default_agent", - "homeassistant.components.denonavr.config_flow", - "homeassistant.components.denonavr.media_player", - "homeassistant.components.denonavr.receiver", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", "homeassistant.components.evohome.water_heater", From ba7ad1029ca6ef7e650404e948ce546a7e02c046 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 7 Jul 2022 05:17:13 -0500 Subject: [PATCH 180/820] Remove legacy Sonos grouping services (#74476) --- .../components/sonos/media_player.py | 35 +-------------- homeassistant/components/sonos/services.yaml | 29 ------------- tests/components/sonos/test_media_player.py | 17 -------- tests/components/sonos/test_services.py | 43 +++++++++++++++++++ 4 files changed, 44 insertions(+), 80 deletions(-) create mode 100644 tests/components/sonos/test_services.py diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 470386c341d..4e7998c8e4e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -82,8 +82,6 @@ SONOS_TO_REPEAT = {meaning: mode for mode, meaning in REPEAT_TO_SONOS.items()} UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"] -SERVICE_JOIN = "join" -SERVICE_UNJOIN = "unjoin" SERVICE_SNAPSHOT = "snapshot" SERVICE_RESTORE = "restore" SERVICE_SET_TIMER = "set_sleep_timer" @@ -130,24 +128,7 @@ async def async_setup_entry( assert isinstance(entity, SonosMediaPlayerEntity) speakers.append(entity.speaker) - if service_call.service == SERVICE_JOIN: - _LOGGER.warning( - "Service 'sonos.join' is deprecated and will be removed in 2022.8, please use 'media_player.join'" - ) - master = platform.entities.get(service_call.data[ATTR_MASTER]) - if master: - await SonosSpeaker.join_multi(hass, master.speaker, speakers) # type: ignore[arg-type] - else: - _LOGGER.error( - "Invalid master specified for join service: %s", - service_call.data[ATTR_MASTER], - ) - elif service_call.service == SERVICE_UNJOIN: - _LOGGER.warning( - "Service 'sonos.unjoin' is deprecated and will be removed in 2022.8, please use 'media_player.unjoin'" - ) - await SonosSpeaker.unjoin_multi(hass, speakers) # type: ignore[arg-type] - elif service_call.service == SERVICE_SNAPSHOT: + if service_call.service == SERVICE_SNAPSHOT: await SonosSpeaker.snapshot_multi( hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) @@ -160,20 +141,6 @@ async def async_setup_entry( async_dispatcher_connect(hass, SONOS_CREATE_MEDIA_PLAYER, async_create_entities) ) - hass.services.async_register( - SONOS_DOMAIN, - SERVICE_JOIN, - async_service_handle, - cv.make_entity_service_schema({vol.Required(ATTR_MASTER): cv.entity_id}), - ) - - hass.services.async_register( - SONOS_DOMAIN, - SERVICE_UNJOIN, - async_service_handle, - cv.make_entity_service_schema({}), - ) - join_unjoin_schema = cv.make_entity_service_schema( {vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean} ) diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index a172b45ecd9..9d61c20f7cb 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -1,32 +1,3 @@ -join: - name: Join group - description: Group player together. - fields: - master: - name: Master - description: Entity ID of the player that should become the coordinator of the group. - required: true - selector: - entity: - integration: sonos - domain: media_player - entity_id: - name: Entity - description: Name of entity that will join the master. - required: true - selector: - entity: - integration: sonos - domain: media_player - -unjoin: - name: Unjoin group - description: Unjoin the player from a group. - target: - entity: - integration: sonos - domain: media_player - snapshot: name: Snapshot description: Take a snapshot of the media player. diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 425b11fc35c..9c2119c8331 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,25 +1,8 @@ """Tests for the Sonos Media Player platform.""" -import pytest - -from homeassistant.components.sonos import DOMAIN, media_player from homeassistant.const import STATE_IDLE -from homeassistant.core import Context -from homeassistant.exceptions import Unauthorized from homeassistant.helpers import device_registry as dr -async def test_services(hass, async_autosetup_sonos, hass_read_only_user): - """Test join/unjoin requires control access.""" - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - media_player.SERVICE_JOIN, - {"master": "media_player.bla", "entity_id": "media_player.blub"}, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) - - async def test_device_registry(hass, async_autosetup_sonos, soco): """Test sonos device registered in the device registry.""" device_registry = dr.async_get(hass) diff --git a/tests/components/sonos/test_services.py b/tests/components/sonos/test_services.py new file mode 100644 index 00000000000..d0bf3bf3a06 --- /dev/null +++ b/tests/components/sonos/test_services.py @@ -0,0 +1,43 @@ +"""Tests for Sonos services.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.media_player.const import SERVICE_JOIN +from homeassistant.components.sonos.const import DATA_SONOS +from homeassistant.exceptions import HomeAssistantError + + +async def test_media_player_join(hass, async_autosetup_sonos): + """Test join service.""" + valid_entity_id = "media_player.zone_a" + mocked_entity_id = "media_player.mocked" + + # Ensure an error is raised if the entity is unknown + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + MP_DOMAIN, + SERVICE_JOIN, + {"entity_id": valid_entity_id, "group_members": mocked_entity_id}, + blocking=True, + ) + + # Ensure SonosSpeaker.join_multi is called if entity is found + mocked_speaker = Mock() + mock_entity_id_mappings = {mocked_entity_id: mocked_speaker} + + with patch.dict( + hass.data[DATA_SONOS].entity_id_mappings, mock_entity_id_mappings + ), patch( + "homeassistant.components.sonos.speaker.SonosSpeaker.join_multi" + ) as mock_join_multi: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_JOIN, + {"entity_id": valid_entity_id, "group_members": mocked_entity_id}, + blocking=True, + ) + + found_speaker = hass.data[DATA_SONOS].entity_id_mappings[valid_entity_id] + mock_join_multi.assert_called_with(hass, found_speaker, [mocked_speaker]) From 19f33d205cfc6a53b230b5395b7ca1e42b4ae84c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 12:27:25 +0200 Subject: [PATCH 181/820] Fix mix of aiohttp and requests in Bloomsky (#74598) --- homeassistant/components/bloomsky/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index 8311f6cbd96..ed2ce1ebc70 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -3,7 +3,6 @@ from datetime import timedelta from http import HTTPStatus import logging -from aiohttp.hdrs import AUTHORIZATION import requests import voluptuous as vol @@ -67,7 +66,7 @@ class BloomSky: _LOGGER.debug("Fetching BloomSky update") response = requests.get( f"{self.API_URL}?{self._endpoint_argument}", - headers={AUTHORIZATION: self._api_key}, + headers={"Authorization": self._api_key}, timeout=10, ) if response.status_code == HTTPStatus.UNAUTHORIZED: From 4fcf8280f6efd59dec63b00d49c0e2861417ae13 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 12:31:19 +0200 Subject: [PATCH 182/820] Use FlowResultType enum in Tuya tests (#74596) --- tests/components/tuya/test_config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 46db598e1d8..82e7bfa4b51 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -84,7 +84,7 @@ async def test_user_flow( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" tuya().connect = MagicMock(side_effect=side_effects) @@ -95,7 +95,7 @@ async def test_user_flow( country = [country for country in TUYA_COUNTRIES if country.name == MOCK_COUNTRY][0] - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_USERNAME assert result["data"][CONF_ACCESS_ID] == MOCK_ACCESS_ID assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET @@ -115,7 +115,7 @@ async def test_error_on_invalid_credentials(hass, tuya): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" tuya().connect = MagicMock(return_value=RESPONSE_ERROR) From 2169b70874c06c9ba3fe59e1d40690faf4634470 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 12:31:54 +0200 Subject: [PATCH 183/820] Use FlowResultType enum in WLED tests (#74594) --- tests/components/wled/test_config_flow.py | 34 ++++++++++------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index e1cf08069da..600bf0eb0d2 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.wled.const import CONF_KEEP_MASTER_LIGHT, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result = await hass.config_entries.flow.async_configure( @@ -35,7 +31,7 @@ async def test_full_user_flow_implementation( ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -68,7 +64,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result.get("description_placeholders") == {CONF_NAME: "WLED RGB Light"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +72,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result2.get("title") == "WLED RGB Light" - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -106,7 +102,7 @@ async def test_zeroconf_during_onboarding( ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_HOST: "192.168.1.123"} assert "result" in result @@ -127,7 +123,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -152,7 +148,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -169,7 +165,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -186,7 +182,7 @@ async def test_user_with_cct_channel_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -211,7 +207,7 @@ async def test_zeroconf_without_mac_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -236,7 +232,7 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -261,7 +257,7 @@ async def test_zeroconf_with_cct_channel_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -273,7 +269,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -282,7 +278,7 @@ async def test_options_flow( user_input={CONF_KEEP_MASTER_LIGHT: True}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_KEEP_MASTER_LIGHT: True, } From 996544da2d3f47ef9ea0392f66a9e3483e2e1852 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Jul 2022 14:51:16 +0200 Subject: [PATCH 184/820] Poll cast groups when media player is added or reconnected (#74610) --- homeassistant/components/cast/helpers.py | 2 +- homeassistant/components/cast/media_player.py | 13 ++++ tests/components/cast/test_media_player.py | 75 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index d7419f69563..48f57c39bd5 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -37,7 +37,7 @@ class ChromecastInfo: @property def friendly_name(self) -> str: - """Return the UUID.""" + """Return the Friendly Name.""" return self.cast_info.friendly_name @property diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 958c53ae394..c8a6a82571e 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -441,6 +441,19 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): connection_status.status, ) self._attr_available = new_available + if new_available and not self._cast_info.is_audio_group: + # Poll current group status + for group_uuid in self.mz_mgr.get_multizone_memberships( + self._cast_info.uuid + ): + group_media_controller = self.mz_mgr.get_multizone_mediacontroller( + group_uuid + ) + if not group_media_controller: + continue + self.multizone_new_media_status( + group_uuid, group_media_controller.status + ) self.schedule_update_ha_state() def multizone_new_media_status(self, group_uuid, media_status): diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 61a6067578a..9d3e1a6d534 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -733,6 +733,20 @@ async def test_entity_availability(hass: HomeAssistant): state = hass.states.get(entity_id) assert state.state == "off" + connection_status = MagicMock() + connection_status.status = "LOST" + conn_status_cb(connection_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "unavailable" + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "off" + connection_status = MagicMock() connection_status.status = "DISCONNECTED" conn_status_cb(connection_status) @@ -740,6 +754,14 @@ async def test_entity_availability(hass: HomeAssistant): state = hass.states.get(entity_id) assert state.state == "unavailable" + # Can't reconnect after receiving DISCONNECTED + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "unavailable" + @pytest.mark.parametrize("port,entry_type", ((8009, None), (12345, None))) async def test_device_registry(hass: HomeAssistant, hass_ws_client, port, entry_type): @@ -1677,6 +1699,59 @@ async def test_group_media_states(hass, mz_mock): assert state.state == "playing" +async def test_group_media_states_early(hass, mz_mock): + """Test media states are read from group if entity has no state. + + This tests case asserts group state is polled when the player is created. + """ + entity_id = "media_player.speaker" + reg = er.async_get(hass) + + info = get_fake_chromecast_info() + + mz_mock.get_multizone_memberships = MagicMock(return_value=[str(FakeGroupUUID)]) + mz_mock.get_multizone_mediacontroller = MagicMock( + return_value=MagicMock(status=MagicMock(images=None, player_state="BUFFERING")) + ) + + chromecast, _ = await async_setup_media_player_cast(hass, info) + _, conn_status_cb, _, _ = get_status_callbacks(chromecast, mz_mock) + + state = hass.states.get(entity_id) + assert state is not None + assert state.name == "Speaker" + assert state.state == "unavailable" + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) + + # Check group state is polled when player is first created + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "buffering" + + connection_status = MagicMock() + connection_status.status = "LOST" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "unavailable" + + # Check group state is polled when player reconnects + mz_mock.get_multizone_mediacontroller = MagicMock( + return_value=MagicMock(status=MagicMock(images=None, player_state="PLAYING")) + ) + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "playing" + + async def test_group_media_control(hass, mz_mock, quick_play_mock): """Test media controls are handled by group if entity has no state.""" entity_id = "media_player.speaker" From 6540ba623978813668a30e5822b97e076fc05a93 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:14:36 +0200 Subject: [PATCH 185/820] Remove hassio from mypy ignore list (#74603) * Remove hassio from mypy ignore list * Avoid if TYPE_CHECKING --- homeassistant/components/hassio/__init__.py | 7 +++++-- homeassistant/components/hassio/auth.py | 1 + .../components/hassio/binary_sensor.py | 2 +- homeassistant/components/hassio/ingress.py | 3 +++ homeassistant/components/hassio/sensor.py | 2 +- .../components/hassio/system_health.py | 4 ++++ .../components/hassio/websocket_api.py | 9 ++++---- mypy.ini | 21 ------------------- script/hassfest/mypy_config.py | 7 ------- 9 files changed, 20 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index cd3c704d4c9..5b5cc48eed8 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -504,7 +504,7 @@ def is_hassio(hass: HomeAssistant) -> bool: @callback -def get_supervisor_ip() -> str: +def get_supervisor_ip() -> str | None: """Return the supervisor ip address.""" if "SUPERVISOR" not in os.environ: return None @@ -537,6 +537,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if (data := await store.async_load()) is None: data = {} + assert isinstance(data, dict) + refresh_token = None if "hassio_user" in data: user = await hass.auth.async_get_user(data["hassio_user"]) @@ -710,6 +712,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async_setup_discovery_view(hass, hassio) # Init auth Hass.io feature + assert user is not None async_setup_auth_view(hass, user) # Init ingress Hass.io feature @@ -877,7 +880,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): except HassioAPIError as err: raise UpdateFailed(f"Error on Supervisor API: {err}") from err - new_data = {} + new_data: dict[str, Any] = {} supervisor_info = get_supervisor_info(self.hass) addons_info = get_addons_info(self.hass) addons_stats = get_addons_stats(self.hass) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index f52a8ef0617..37687ee70df 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -43,6 +43,7 @@ class HassIOBaseAuth(HomeAssistantView): """Check if this call is from Supervisor.""" # Check caller IP hassio_ip = os.environ["SUPERVISOR"].split(":")[0] + assert request.transport if ip_address(request.transport.get_extra_info("peername")[0]) != ip_address( hassio_ip ): diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index c2bcd5eaf68..85cb402b0ca 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -59,7 +59,7 @@ async def async_setup_entry( """Binary sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities = [] + entities: list[HassioAddonBinarySensor | HassioOSBinarySensor] = [] for entity_description in ADDON_ENTITY_DESCRIPTIONS: for addon in coordinator.data[DATA_KEY_ADDONS].values(): diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 6caa97b788f..8aacbac99f6 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from ipaddress import ip_address import logging import os @@ -73,6 +74,7 @@ class HassIOIngress(HomeAssistantView): self, request: web.Request, token: str, path: str ) -> web.WebSocketResponse: """Ingress route for websocket.""" + req_protocols: Iterable[str] if hdrs.SEC_WEBSOCKET_PROTOCOL in request.headers: req_protocols = [ str(proto.strip()) @@ -190,6 +192,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st # Set X-Forwarded-For forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) + assert request.transport if (peername := request.transport.get_extra_info("peername")) is None: _LOGGER.error("Can't set forward_for header, missing peername") raise HTTPBadRequest() diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 42be1ff4b0a..55fcb0bcd28 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -65,7 +65,7 @@ async def async_setup_entry( """Sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities = [] + entities: list[HassioOSSensor | HassioAddonSensor] = [] for addon in coordinator.data[DATA_KEY_ADDONS].values(): for entity_description in ADDON_ENTITY_DESCRIPTIONS: diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index b1fc208de80..d8d29f44d68 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -1,4 +1,6 @@ """Provide info to system health.""" +from __future__ import annotations + import os from homeassistant.components import system_health @@ -24,6 +26,7 @@ async def system_health_info(hass: HomeAssistant): host_info = get_host_info(hass) supervisor_info = get_supervisor_info(hass) + healthy: bool | dict[str, str] if supervisor_info.get("healthy"): healthy = True else: @@ -32,6 +35,7 @@ async def system_health_info(hass: HomeAssistant): "error": "Unhealthy", } + supported: bool | dict[str, str] if supervisor_info.get("supported"): supported = True else: diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 7eb037d8432..b25d9fda7a0 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -1,5 +1,6 @@ """Websocekt API handlers for the hassio integration.""" import logging +from numbers import Number import re import voluptuous as vol @@ -56,8 +57,8 @@ def async_load_websocket_api(hass: HomeAssistant): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(WS_TYPE): WS_TYPE_SUBSCRIBE}) +@websocket_api.async_response async def websocket_subscribe( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): @@ -74,13 +75,13 @@ async def websocket_subscribe( connection.send_message(websocket_api.result_message(msg[WS_ID])) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(WS_TYPE): WS_TYPE_EVENT, vol.Required(ATTR_DATA): SCHEMA_WEBSOCKET_EVENT, } ) +@websocket_api.async_response async def websocket_supervisor_event( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): @@ -89,16 +90,16 @@ async def websocket_supervisor_event( connection.send_result(msg[WS_ID]) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(WS_TYPE): WS_TYPE_API, vol.Required(ATTR_ENDPOINT): cv.string, vol.Required(ATTR_METHOD): cv.string, vol.Optional(ATTR_DATA): dict, - vol.Optional(ATTR_TIMEOUT): vol.Any(cv.Number, None), + vol.Optional(ATTR_TIMEOUT): vol.Any(Number, None), } ) +@websocket_api.async_response async def websocket_supervisor_api( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): diff --git a/mypy.ini b/mypy.ini index c4a135322fe..fc99119cda3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2653,27 +2653,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.water_heater] ignore_errors = true -[mypy-homeassistant.components.hassio] -ignore_errors = true - -[mypy-homeassistant.components.hassio.auth] -ignore_errors = true - -[mypy-homeassistant.components.hassio.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.hassio.ingress] -ignore_errors = true - -[mypy-homeassistant.components.hassio.sensor] -ignore_errors = true - -[mypy-homeassistant.components.hassio.system_health] -ignore_errors = true - -[mypy-homeassistant.components.hassio.websocket_api] -ignore_errors = true - [mypy-homeassistant.components.icloud] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e7c5e0ed129..7f1faf00bb6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -26,13 +26,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.evohome", "homeassistant.components.evohome.climate", "homeassistant.components.evohome.water_heater", - "homeassistant.components.hassio", - "homeassistant.components.hassio.auth", - "homeassistant.components.hassio.binary_sensor", - "homeassistant.components.hassio.ingress", - "homeassistant.components.hassio.sensor", - "homeassistant.components.hassio.system_health", - "homeassistant.components.hassio.websocket_api", "homeassistant.components.icloud", "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", From dfdd0378784ab93c26138ccaa59e904f8c6fad89 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 16:02:36 +0200 Subject: [PATCH 186/820] Update aiokafka to 0.7.2 (#74601) --- homeassistant/components/apache_kafka/__init__.py | 1 - homeassistant/components/apache_kafka/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index 1b293bd2c04..38a70b450ab 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -102,7 +102,6 @@ class KafkaManager: self._hass = hass ssl_context = ssl_util.client_context() self._producer = AIOKafkaProducer( - loop=hass.loop, bootstrap_servers=f"{ip_address}:{port}", compression_type="gzip", security_protocol=security_protocol, diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index 3b290146a09..3fa1f70c57b 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -2,7 +2,7 @@ "domain": "apache_kafka", "name": "Apache Kafka", "documentation": "https://www.home-assistant.io/integrations/apache_kafka", - "requirements": ["aiokafka==0.6.0"], + "requirements": ["aiokafka==0.7.2"], "codeowners": ["@bachya"], "iot_class": "local_push", "loggers": ["aiokafka", "kafka_python"] diff --git a/requirements_all.txt b/requirements_all.txt index 97d62c96420..5970f541f8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -181,7 +181,7 @@ aiohue==4.4.2 aioimaplib==1.0.0 # homeassistant.components.apache_kafka -aiokafka==0.6.0 +aiokafka==0.7.2 # homeassistant.components.kef aiokef==0.2.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06dcf27cbe5..b07ebcd33b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -162,7 +162,7 @@ aiohttp_cors==0.7.0 aiohue==4.4.2 # homeassistant.components.apache_kafka -aiokafka==0.6.0 +aiokafka==0.7.2 # homeassistant.components.lookin aiolookin==0.1.1 From 29cbd9d469df1d4b0a1712464b7551d53c8f2c93 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 7 Jul 2022 16:19:56 +0200 Subject: [PATCH 187/820] Update frontend to 20220707.0 (#74625) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 85ead380485..5c7fd1a20be 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220706.0"], + "requirements": ["home-assistant-frontend==20220707.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d72cb6445a5..a388f178df3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220706.0 +home-assistant-frontend==20220707.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5970f541f8e..838e1278d75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220706.0 +home-assistant-frontend==20220707.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b07ebcd33b4..b9c98ba9fb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220706.0 +home-assistant-frontend==20220707.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 4e2de2479a3f1ae861c7683fe3c5c0881a242c17 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 7 Jul 2022 15:25:44 +0100 Subject: [PATCH 188/820] Add SetSystemDateandTime Button (#66419) * add SetSystemDateandTime * fix * address review * follow recommendation to set date and time on start * add set date and time button test --- homeassistant/components/onvif/button.py | 19 ++++++++++++-- homeassistant/components/onvif/device.py | 32 ++++++++++++++++++++++++ tests/components/onvif/test_button.py | 30 ++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/button.py b/homeassistant/components/onvif/button.py index 23ea5124e61..0af4a16d269 100644 --- a/homeassistant/components/onvif/button.py +++ b/homeassistant/components/onvif/button.py @@ -1,5 +1,4 @@ """ONVIF Buttons.""" - from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,7 +17,7 @@ async def async_setup_entry( ) -> None: """Set up ONVIF button based on a config entry.""" device = hass.data[DOMAIN][config_entry.unique_id] - async_add_entities([RebootButton(device)]) + async_add_entities([RebootButton(device), SetSystemDateAndTimeButton(device)]) class RebootButton(ONVIFBaseEntity, ButtonEntity): @@ -39,3 +38,19 @@ class RebootButton(ONVIFBaseEntity, ButtonEntity): """Send out a SystemReboot command.""" device_mgmt = self.device.device.create_devicemgmt_service() await device_mgmt.SystemReboot() + + +class SetSystemDateAndTimeButton(ONVIFBaseEntity, ButtonEntity): + """Defines a ONVIF SetSystemDateAndTime button.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, device: ONVIFDevice) -> None: + """Initialize the button entity.""" + super().__init__(device) + self._attr_name = f"{self.device.name} Set System Date and Time" + self._attr_unique_id = f"{self.device.info.mac or self.device.info.serial_number}_setsystemdatetime" + + async def async_press(self) -> None: + """Send out a SetSystemDateAndTime command.""" + await self.device.async_manually_set_date_and_time() diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index d376b7fe258..5907ea90124 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -5,6 +5,7 @@ import asyncio from contextlib import suppress import datetime as dt import os +import time from httpx import RequestError import onvif @@ -148,6 +149,32 @@ class ONVIFDevice: await self.events.async_stop() await self.device.close() + async def async_manually_set_date_and_time(self) -> None: + """Set Date and Time Manually using SetSystemDateAndTime command.""" + device_mgmt = self.device.create_devicemgmt_service() + + # Retrieve DateTime object from camera to use as template for Set operation + device_time = await device_mgmt.GetSystemDateAndTime() + + system_date = dt_util.utcnow() + LOGGER.debug("System date (UTC): %s", system_date) + + dt_param = device_mgmt.create_type("SetSystemDateAndTime") + dt_param.DateTimeType = "Manual" + # Retrieve DST setting from system + dt_param.DaylightSavings = bool(time.localtime().tm_isdst) + dt_param.UTCDateTime = device_time.UTCDateTime + # Retrieve timezone from system + dt_param.TimeZone = str(system_date.astimezone().tzinfo) + dt_param.UTCDateTime.Date.Year = system_date.year + dt_param.UTCDateTime.Date.Month = system_date.month + dt_param.UTCDateTime.Date.Day = system_date.day + dt_param.UTCDateTime.Time.Hour = system_date.hour + dt_param.UTCDateTime.Time.Minute = system_date.minute + dt_param.UTCDateTime.Time.Second = system_date.second + LOGGER.debug("SetSystemDateAndTime: %s", dt_param) + await device_mgmt.SetSystemDateAndTime(dt_param) + async def async_check_date_and_time(self) -> None: """Warns if device and system date not synced.""" LOGGER.debug("Setting up the ONVIF device management service") @@ -165,6 +192,8 @@ class ONVIFDevice: ) return + LOGGER.debug("Device time: %s", device_time) + tzone = dt_util.DEFAULT_TIME_ZONE cdate = device_time.LocalDateTime if device_time.UTCDateTime: @@ -207,6 +236,9 @@ class ONVIFDevice: cam_date_utc, system_date, ) + if device_time.DateTimeType == "Manual": + # Set Date and Time ourselves if Date and Time is set manually in the camera. + await self.async_manually_set_date_and_time() except RequestError as err: LOGGER.warning( "Couldn't get device '%s' date/time. Error: %s", self.name, err diff --git a/tests/components/onvif/test_button.py b/tests/components/onvif/test_button.py index a8ac24da524..be418acd1e0 100644 --- a/tests/components/onvif/test_button.py +++ b/tests/components/onvif/test_button.py @@ -38,3 +38,33 @@ async def test_reboot_button_press(hass): await hass.async_block_till_done() devicemgmt.SystemReboot.assert_called_once() + + +async def test_set_dateandtime_button(hass): + """Test states of the SetDateAndTime button.""" + await setup_onvif_integration(hass) + + state = hass.states.get("button.testcamera_set_system_date_and_time") + assert state + assert state.state == STATE_UNKNOWN + + registry = er.async_get(hass) + entry = registry.async_get("button.testcamera_set_system_date_and_time") + assert entry + assert entry.unique_id == f"{MAC}_setsystemdatetime" + + +async def test_set_dateandtime_button_press(hass): + """Test SetDateAndTime button press.""" + _, camera, device = await setup_onvif_integration(hass) + device.async_manually_set_date_and_time = AsyncMock(return_value=True) + + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {ATTR_ENTITY_ID: "button.testcamera_set_system_date_and_time"}, + blocking=True, + ) + await hass.async_block_till_done() + + device.async_manually_set_date_and_time.assert_called_once() From c01f7d75d5ec8355a14f74fee1ded20a7340b624 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 16:59:49 +0200 Subject: [PATCH 189/820] Fix mix of aiohttp and requests in ZAMG (#74628) --- homeassistant/components/zamg/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 87a1175b7cd..c32aa942625 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -10,7 +10,6 @@ import logging import os from typing import Union -from aiohttp.hdrs import USER_AGENT import requests import voluptuous as vol @@ -275,7 +274,7 @@ class ZamgData: """The class for handling the data retrieval.""" API_URL = "http://www.zamg.ac.at/ogd/" - API_HEADERS = {USER_AGENT: f"home-assistant.zamg/ {__version__}"} + API_HEADERS = {"User-Agent": f"home-assistant.zamg/ {__version__}"} def __init__(self, station_id): """Initialize the probe.""" From 323d4a0e1bf08f1bc3c5fc627889b4aaafa7d5c5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 17:25:23 +0200 Subject: [PATCH 190/820] Use FlowResultType enum in Plugwise tests (#74638) --- tests/components/plugwise/test_config_flow.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index f2a2ef3bc43..66f0986682e 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -21,11 +21,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -88,7 +84,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -102,7 +98,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -136,7 +132,7 @@ async def test_zeroconf_flow( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=discovery, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -147,7 +143,7 @@ async def test_zeroconf_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -172,7 +168,7 @@ async def test_zeroconf_flow_stretch( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY2, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -183,7 +179,7 @@ async def test_zeroconf_flow_stretch( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -224,7 +220,7 @@ async def test_zercoconf_discovery_update_configuration( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "0.0.0.0" @@ -235,7 +231,7 @@ async def test_zercoconf_discovery_update_configuration( data=TEST_DISCOVERY, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "1.1.1.1" @@ -262,7 +258,7 @@ async def test_flow_errors( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -273,7 +269,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": reason} assert result2.get("step_id") == "user" @@ -286,7 +282,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Test Smile Name" assert result3.get("data") == { CONF_HOST: TEST_HOST, From 1dd9e705f2ea696b1cf8cbee178c374455c04384 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jul 2022 10:39:05 -0500 Subject: [PATCH 191/820] Switch dispatcher to use async_run_hass_job (#74514) * Switch dispatcher to use async_run_hass_job - Since we already wrap all the callbacks in catch_log_exception we can use async_run_hass_job here - The overhead of wrapping the call in a call_soon, queuing it and running it later usually exceeds the overhead of running the job itself * fix size change during iteration * fix out of order send * fix missing mocking in unifi test * Fix Legrand Home+ Control updating entities before the coordinator update had finished * stray debug --- .../components/hassio/websocket_api.py | 2 +- .../components/home_plus_control/__init__.py | 34 +++++++++++-------- homeassistant/helpers/dispatcher.py | 12 ++++++- tests/components/unifi/test_controller.py | 17 +++++++++- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index b25d9fda7a0..eb0d6c54077 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -86,8 +86,8 @@ async def websocket_supervisor_event( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): """Publish events from the Supervisor.""" - async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA]) connection.send_result(msg[WS_ID]) + async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA]) @websocket_api.websocket_command( diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index e222b65e293..fbae1ae970a 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, @@ -102,12 +102,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(10): - module_data = await api.async_get_modules() + return await api.async_get_modules() except HomePlusControlApiError as err: raise UpdateFailed( f"Error communicating with API: {err} [{type(err)}]" ) from err + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="home_plus_control_module", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=300), + ) + hass_entry_data[DATA_COORDINATOR] = coordinator + + @callback + def _async_update_entities(): + """Process entities and add or remove them based after an update.""" + if not (module_data := coordinator.data): + return + # Remove obsolete entities from Home Assistant entity_uids_to_remove = uids - set(module_data) for uid in entity_uids_to_remove: @@ -126,18 +143,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator, ) - return module_data - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name="home_plus_control_module", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=300), - ) - hass_entry_data[DATA_COORDINATOR] = coordinator + entry.async_on_unload(coordinator.async_add_listener(_async_update_entities)) async def start_platforms(): """Continue setting up the platforms.""" diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 12a6616b009..07a3e3b3b28 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -85,8 +85,18 @@ def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: This method must be run in the event loop. """ target_list = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) + + run: list[HassJob] = [] for target, job in target_list.items(): if job is None: job = _generate_job(signal, target) target_list[target] = job - hass.async_add_hass_job(job, *args) + + # Run the jobs all at the end + # to ensure no jobs add more disptachers + # which can result in the target_list + # changing size during iteration + run.append(job) + + for job in run: + hass.async_run_hass_job(job, *args) diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 8a41ada9b62..625afbb4ec6 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -379,7 +379,21 @@ async def test_wireless_client_event_calls_update_wireless_devices( hass, aioclient_mock, mock_unifi_websocket ): """Call update_wireless_devices method when receiving wireless client event.""" - await setup_unifi_integration(hass, aioclient_mock) + client_1_dict = { + "essid": "ssid", + "disabled": False, + "hostname": "client_1", + "ip": "10.0.0.4", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + } + await setup_unifi_integration( + hass, + aioclient_mock, + clients_response=[client_1_dict], + known_wireless_clients=(client_1_dict["mac"],), + ) with patch( "homeassistant.components.unifi.controller.UniFiController.update_wireless_clients", @@ -391,6 +405,7 @@ async def test_wireless_client_event_calls_update_wireless_devices( "data": [ { "datetime": "2020-01-20T19:37:04Z", + "user": "00:00:00:00:00:01", "key": aiounifi.events.WIRELESS_CLIENT_CONNECTED, "msg": "User[11:22:33:44:55:66] has connected to WLAN", "time": 1579549024893, From 68ccb9608973262309ed4f7709bc27d03447b313 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 18:53:24 +0200 Subject: [PATCH 192/820] Refactor CI (#74014) --- .github/workflows/ci.yaml | 459 +++++++++--------- tests/components/homekit/test_type_cameras.py | 1 - tests/components/motioneye/test_camera.py | 8 +- tests/components/stream/test_recorder.py | 8 +- tests/components/stream/test_worker.py | 10 +- 5 files changed, 241 insertions(+), 245 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1d7a5c95986..6a4413e3668 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,8 +20,8 @@ on: type: boolean env: - CACHE_VERSION: 10 - PIP_CACHE_VERSION: 4 + CACHE_VERSION: 0 + PIP_CACHE_VERSION: 0 HA_SHORT_VERSION: 2022.8 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit @@ -35,24 +35,38 @@ concurrency: cancel-in-progress: true jobs: - changes: - name: Determine what has changed + info: + name: Collect information & changes data outputs: # In case of issues with the partial run, use the following line instead: # test_full_suite: 'true' - test_full_suite: ${{ steps.info.outputs.test_full_suite }} core: ${{ steps.core.outputs.changes }} - integrations: ${{ steps.integrations.outputs.changes }} integrations_glob: ${{ steps.info.outputs.integrations_glob }} - tests: ${{ steps.info.outputs.tests }} - tests_glob: ${{ steps.info.outputs.tests_glob }} - test_groups: ${{ steps.info.outputs.test_groups }} - test_group_count: ${{ steps.info.outputs.test_group_count }} + integrations: ${{ steps.integrations.outputs.changes }} + pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }} + python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }} requirements: ${{ steps.core.outputs.requirements }} - runs-on: ubuntu-latest + test_full_suite: ${{ steps.info.outputs.test_full_suite }} + test_group_count: ${{ steps.info.outputs.test_group_count }} + test_groups: ${{ steps.info.outputs.test_groups }} + tests_glob: ${{ steps.info.outputs.tests_glob }} + tests: ${{ steps.info.outputs.tests }} + runs-on: ubuntu-20.04 steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 + - name: Generate partial Python venv restore key + id: generate_python_cache_key + run: >- + echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_all.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }}" + - name: Generate partial pre-commit restore key + id: generate_pre-commit_cache_key + run: >- + echo "::set-output name=key::${{ env.CACHE_VERSION }}-${{ env.DEFAULT_PYTHON }}-${{ + hashFiles('.pre-commit-config.yaml') }}" - name: Filter for core changes uses: dorny/paths-filter@v2.10.2 id: core @@ -142,15 +156,11 @@ jobs: echo "tests_glob: ${tests_glob}" echo "::set-output name=tests_glob::${tests_glob}" - # Separate job to pre-populate the base dependency cache - # This prevent upcoming jobs to do the same individually - prepare-base: - name: Prepare base dependencies - runs-on: ubuntu-latest - timeout-minutes: 20 - outputs: - python-key: ${{ steps.generate-python-key.outputs.key }} - pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} + pre-commit: + name: Prepare pre-commit base + runs-on: ubuntu-20.04 + needs: + - info steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -159,67 +169,26 @@ jobs: uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }}" - - name: Generate partial pip restore key - id: generate-pip-key - run: >- - echo "::set-output name=key::base-pip-${{ env.PIP_CACHE_VERSION }}-${{ - env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" + cache: "pip" - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-python-key.outputs.key }} - # Temporary disabling the restore of environments when bumping - # a dependency. It seems that we are experiencing issues with - # restoring environments in GitHub Actions, although unclear why. - # First attempt: https://github.com/home-assistant/core/pull/62383 - # - # restore-keys: | - # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- - # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- - # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - - name: Restore pip wheel cache - if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.4 - with: - path: ${{ env.PIP_CACHE }} - key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-pip-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel - pip install --cache-dir=$PIP_CACHE -r requirements.txt -r requirements_test.txt --use-deprecated=legacy-resolver - - name: Generate partial pre-commit restore key - id: generate-pre-commit-key - run: >- - echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ - hashFiles('.pre-commit-config.yaml') }}" + pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: >- - ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}- + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -228,10 +197,10 @@ jobs: lint-black: name: Check black - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-base + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -245,8 +214,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -257,31 +225,31 @@ jobs: uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | echo "Failed to restore pre-commit environment from cache" exit 1 - name: Run black (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual black --all-files --show-diff-on-failure - name: Run black (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure + pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* --show-diff-on-failure lint-flake8: name: Check flake8 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-base + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -295,8 +263,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -307,7 +274,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -317,22 +284,24 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/flake8.json" - name: Run flake8 (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual flake8 --all-files - name: Run flake8 (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* + pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* lint-isort: name: Check isort - runs-on: ubuntu-latest - needs: prepare-base + runs-on: ubuntu-20.04 + needs: + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -346,8 +315,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -358,7 +326,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -371,10 +339,10 @@ jobs: lint-other: name: Check other linters - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-base + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -388,8 +356,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -400,7 +367,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -408,17 +375,17 @@ jobs: exit 1 - name: Run pyupgrade (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure - name: Run pyupgrade (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual pyupgrade --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure + pre-commit run --hook-stage manual pyupgrade --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* --show-diff-on-failure - name: Register yamllint problem matcher run: | @@ -437,17 +404,17 @@ jobs: pre-commit run --hook-stage manual check-json --all-files - name: Run prettier (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual prettier --all-files - name: Run prettier (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate - pre-commit run --hook-stage manual prettier --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* + pre-commit run --hook-stage manual prettier --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* - name: Register check executables problem matcher run: | @@ -478,36 +445,105 @@ jobs: args: hadolint Dockerfile.dev - name: Run bandit (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure - name: Run bandit (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure + pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* --show-diff-on-failure - hassfest: - name: Check hassfest - runs-on: ubuntu-latest - needs: prepare-tests + base: + name: Prepare dependencies + runs-on: ubuntu-20.04 + needs: info + timeout-minutes: 60 strategy: matrix: - python-version: [3.9] - container: homeassistant/ci-azure:${{ matrix.python-version }} + python-version: ["3.9", "3.10"] steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ matrix.python-version }} + - name: Generate partial pip restore key + id: generate-pip-key + run: >- + echo "::set-output name=key::pip-${{ env.PIP_CACHE_VERSION }}-${{ + env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" + - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} + - name: Restore pip wheel cache + if: steps.cache-venv.outputs.cache-hit != 'true' + uses: actions/cache@v3.0.4 + with: + path: ${{ env.PIP_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-pip-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + - name: Install additional OS dependencies + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg \ + libavcodec-dev \ + libavdevice-dev \ + libavfilter-dev \ + libavformat-dev \ + libavutil-dev \ + libswresample-dev \ + libswscale-dev \ + libudev-dev + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python --version + pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel + pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver + pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver + pip install -e . + + hassfest: + name: Check hassfest + runs-on: ubuntu-20.04 + needs: + - info + - base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.0.2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment + id: cache-venv + uses: actions/cache@v3.0.4 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -520,14 +556,16 @@ jobs: gen-requirements-all: name: Check all requirements - runs-on: ubuntu-latest - needs: prepare-base + runs-on: ubuntu-20.04 + needs: + - info + - base steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 id: python + uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -535,8 +573,9 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -547,94 +586,29 @@ jobs: . venv/bin/activate python -m script.gen_requirements_all validate - prepare-tests: - name: Prepare tests for Python ${{ matrix.python-version }} - runs-on: ubuntu-latest - timeout-minutes: 60 - strategy: - matrix: - python-version: ["3.9", "3.10"] - outputs: - python-key: ${{ steps.generate-python-key.outputs.key }} - container: homeassistant/ci-azure:${{ matrix.python-version }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }}" - - name: Generate partial pip restore key - id: generate-pip-key - run: >- - echo "::set-output name=key::pip-${{ env.PIP_CACHE_VERSION }}-${{ - env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache@v3.0.4 - with: - path: venv - key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ - steps.generate-python-key.outputs.key }} - # Temporary disabling the restore of environments when bumping - # a dependency. It seems that we are experiencing issues with - # restoring environments in GitHub Actions, although unclear why. - # First attempt: https://github.com/home-assistant/core/pull/62383 - # - # restore-keys: | - # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- - # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- - # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - - name: Restore pip wheel cache - if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.4 - with: - path: ${{ env.PIP_CACHE }} - key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ - steps.generate-pip-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - - name: Create full Python ${{ matrix.python-version }} virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - # Temporary addition of cmake, needed to build some Python 3.9 packages - apt-get update - apt-get -y install cmake - - python -m venv venv - . venv/bin/activate - python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel - pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver - pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver - pip install -e . - pylint: name: Check pylint - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 timeout-minutes: 20 needs: - - changes - - prepare-tests - strategy: - matrix: - python-version: [3.9] - container: homeassistant/ci-azure:${{ matrix.python-version }} + - info + - base steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -644,39 +618,41 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pylint.json" - name: Run pylint (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate python --version pylint --ignore-missing-annotations=y homeassistant - name: Run pylint (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate python --version - pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.changes.outputs.integrations_glob }} + pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }} mypy: name: Check mypy - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-tests - strategy: - matrix: - python-version: [3.9] - container: homeassistant/ci-azure:${{ matrix.python-version }} + - info + - base steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -686,41 +662,46 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/mypy.json" - name: Run mypy (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate python --version mypy homeassistant pylint - name: Run mypy (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate python --version - mypy homeassistant/components/${{ needs.changes.outputs.integrations_glob }} + mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }} pip-check: - runs-on: ubuntu-latest - if: needs.changes.outputs.requirements == 'true' || github.event.inputs.full == 'true' + runs-on: ubuntu-20.04 + if: needs.info.outputs.requirements == 'true' || github.event.inputs.full == 'true' needs: - - changes - - prepare-tests + - info + - base strategy: fail-fast: false matrix: - python-version: [3.9] + python-version: ["3.9", "3.10"] name: Run pip check ${{ matrix.python-version }} - container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -732,38 +713,48 @@ jobs: ./script/pip_check $PIP_CACHE pytest: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' - && (needs.changes.outputs.test_full_suite == 'true' || needs.changes.outputs.tests_glob) + && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob) needs: - - changes + - info + - base - gen-requirements-all - hassfest - lint-black - lint-other - lint-isort - mypy - - prepare-tests strategy: fail-fast: false matrix: - group: ${{ fromJson(needs.changes.outputs.test_groups) }} + group: ${{ fromJson(needs.info.outputs.test_groups) }} python-version: ["3.9", "3.10"] name: >- Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) - container: homeassistant/ci-azure:${{ matrix.python-version }} steps: + - name: Install additional OS dependencies + run: | + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg - name: Check out code from GitHub uses: actions/checkout@v3.0.2 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -783,7 +774,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Run pytest (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' timeout-minutes: 60 run: | . venv/bin/activate @@ -794,7 +785,7 @@ jobs: --durations=10 \ -n auto \ --dist=loadfile \ - --test-group-count ${{ needs.changes.outputs.test_group_count }} \ + --test-group-count ${{ needs.info.outputs.test_group_count }} \ --test-group=${{ matrix.group }} \ --cov="homeassistant" \ --cov-report=xml \ @@ -802,8 +793,8 @@ jobs: -p no:sugar \ tests - name: Run pytest (partially) - if: needs.changes.outputs.test_full_suite == 'false' - timeout-minutes: 20 + if: needs.info.outputs.test_full_suite == 'false' + timeout-minutes: 10 shell: bash run: | . venv/bin/activate @@ -838,9 +829,9 @@ jobs: coverage: name: Upload test coverage to Codecov - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes + - info - pytest steps: - name: Check out code from GitHub @@ -848,10 +839,10 @@ jobs: - name: Download all coverage artifacts uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' uses: codecov/codecov-action@v3.1.0 with: flags: full-suite - name: Upload coverage to Codecov (partial coverage) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' uses: codecov/codecov-action@v3.1.0 diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 83afcedd839..f6855ca3cbb 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -501,7 +501,6 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, ): await _async_start_streaming(hass, acc) await _async_reconfigure_stream(hass, acc, session_info, {}) - await _async_stop_stream(hass, acc, session_info) await _async_stop_all_streams(hass, acc) expected_output = ( diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 8ba9fb07715..3e2db3bf897 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -265,12 +265,12 @@ async def test_get_stream_from_camera(aiohttp_server: Any, hass: HomeAssistant) client = create_mock_motioneye_client() client.get_camera_stream_url = Mock( - return_value=f"http://localhost:{stream_server.port}/" + return_value=f"http://127.0.0.1:{stream_server.port}/" ) config_entry = create_mock_motioneye_config_entry( hass, data={ - CONF_URL: f"http://localhost:{stream_server.port}", + CONF_URL: f"http://127.0.0.1:{stream_server.port}", # The port won't be used as the client is a mock. CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, }, @@ -351,13 +351,13 @@ async def test_camera_option_stream_url_template( config_entry = create_mock_motioneye_config_entry( hass, data={ - CONF_URL: f"http://localhost:{stream_server.port}", + CONF_URL: f"http://127.0.0.1:{stream_server.port}", # The port won't be used as the client is a mock. CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, }, options={ CONF_STREAM_URL_TEMPLATE: ( - f"http://localhost:{stream_server.port}/" "{{ name }}/{{ id }}" + f"http://127.0.0.1:{stream_server.port}/" "{{ name }}/{{ id }}" ) }, ) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index d7595b47679..a070f609129 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -71,7 +71,7 @@ async def test_record_stream(hass, filename, h264_video): assert os.path.exists(filename) -async def test_record_lookback(hass, h264_video): +async def test_record_lookback(hass, filename, h264_video): """Exercise record with loopback.""" stream = create_stream(hass, h264_video, {}) @@ -81,7 +81,7 @@ async def test_record_lookback(hass, h264_video): await stream.start() with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path", lookback=4) + await stream.async_record(filename, lookback=4) # This test does not need recorder cleanup since it is not fully exercised @@ -245,10 +245,10 @@ async def test_record_stream_audio( await hass.async_block_till_done() -async def test_recorder_log(hass, caplog): +async def test_recorder_log(hass, filename, caplog): """Test starting a stream to record logs the url without username and password.""" stream = create_stream(hass, "https://abcd:efgh@foo.bar", {}) with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") + await stream.async_record(filename) assert "https://abcd:efgh@foo.bar" not in caplog.text assert "https://****:****@foo.bar" in caplog.text diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 8717e23a476..d887a165b44 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -71,6 +71,12 @@ SEGMENTS_PER_PACKET = PACKET_DURATION / SEGMENT_DURATION TIMEOUT = 15 +@pytest.fixture +def filename(tmpdir): + """Use this filename for the tests.""" + return f"{tmpdir}/test.mp4" + + @pytest.fixture(autouse=True) def mock_stream_settings(hass): """Set the stream settings data in hass before each test.""" @@ -895,7 +901,7 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream): } -async def test_get_image(hass): +async def test_get_image(hass, filename): """Test that the has_keyframe metadata matches the media.""" await async_setup_component(hass, "stream", {"stream": {}}) @@ -909,7 +915,7 @@ async def test_get_image(hass): stream = create_stream(hass, source, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): - make_recording = hass.async_create_task(stream.async_record("/example/path")) + make_recording = hass.async_create_task(stream.async_record(filename)) await make_recording assert stream._keyframe_converter._image is None From 7cd68381f1d4f58930ffd631dfbfc7159d459832 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 18:57:36 +0200 Subject: [PATCH 193/820] Search/replace RESULT_TYPE_* by FlowResultType enum (#74642) --- .../auth/mfa_modules/test_insecure_example.py | 16 +- tests/auth/mfa_modules/test_notify.py | 34 ++-- tests/auth/mfa_modules/test_totp.py | 12 +- tests/auth/providers/test_command_line.py | 8 +- tests/auth/providers/test_homeassistant.py | 16 +- .../providers/test_legacy_api_password.py | 6 +- tests/auth/test_init.py | 30 ++-- tests/components/abode/test_config_flow.py | 14 +- tests/components/abode/test_init.py | 2 +- .../accuweather/test_config_flow.py | 8 +- tests/components/acmeda/test_config_flow.py | 8 +- tests/components/adguard/test_config_flow.py | 18 +- .../advantage_air/test_config_flow.py | 8 +- tests/components/aemet/test_config_flow.py | 12 +- .../components/agent_dvr/test_config_flow.py | 10 +- tests/components/airly/test_config_flow.py | 6 +- tests/components/airnow/test_config_flow.py | 4 +- .../components/airvisual/test_config_flow.py | 26 +-- tests/components/airzone/test_config_flow.py | 10 +- .../alarmdecoder/test_config_flow.py | 60 +++---- tests/components/almond/test_config_flow.py | 12 +- .../amberelectric/test_config_flow.py | 16 +- .../ambiclimate/test_config_flow.py | 18 +- .../ambient_station/test_config_flow.py | 8 +- .../components/androidtv/test_config_flow.py | 58 +++---- tests/components/apple_tv/test_config_flow.py | 102 ++++++------ .../application_credentials/test_init.py | 24 +-- .../components/arcam_fmj/test_config_flow.py | 20 +-- tests/components/asuswrt/test_config_flow.py | 22 +-- tests/components/atag/test_config_flow.py | 12 +- tests/components/aurora/test_config_flow.py | 4 +- .../aurora_abb_powerone/test_config_flow.py | 2 +- .../components/aussie_broadband/test_init.py | 2 +- tests/components/auth/test_mfa_setup_flow.py | 4 +- tests/components/awair/test_config_flow.py | 14 +- .../azure_devops/test_config_flow.py | 34 ++-- .../azure_event_hub/test_config_flow.py | 14 +- tests/components/balboa/test_config_flow.py | 4 +- tests/components/blebox/test_config_flow.py | 2 +- tests/components/blink/test_config_flow.py | 6 +- .../bmw_connected_drive/test_config_flow.py | 10 +- tests/components/braviatv/test_config_flow.py | 18 +- tests/components/brother/test_config_flow.py | 24 +-- tests/components/brunt/test_config_flow.py | 12 +- tests/components/bsblan/test_config_flow.py | 14 +- .../components/buienradar/test_config_flow.py | 2 +- tests/components/cast/test_config_flow.py | 14 +- .../cert_expiry/test_config_flow.py | 28 ++-- .../components/climacell/test_config_flow.py | 4 +- tests/components/cloud/test_account_link.py | 2 +- .../components/config/test_config_entries.py | 2 +- .../components/crownstone/test_config_flow.py | 50 +++--- tests/components/denonavr/test_config_flow.py | 4 +- tests/components/dexcom/test_config_flow.py | 18 +- tests/components/dialogflow/test_init.py | 4 +- tests/components/discord/test_config_flow.py | 22 +-- tests/components/dlna_dmr/test_config_flow.py | 70 ++++---- tests/components/dlna_dms/test_config_flow.py | 36 ++-- tests/components/doorbird/test_config_flow.py | 12 +- tests/components/dsmr/test_config_flow.py | 2 +- tests/components/dunehd/test_config_flow.py | 4 +- tests/components/ecobee/test_config_flow.py | 16 +- tests/components/elmax/test_config_flow.py | 26 +-- tests/components/enocean/test_config_flow.py | 20 +-- .../environment_canada/test_config_flow.py | 6 +- .../components/faa_delays/test_config_flow.py | 2 +- .../fireservicerota/test_config_flow.py | 10 +- .../flick_electric/test_config_flow.py | 10 +- tests/components/flipr/test_config_flow.py | 8 +- .../components/flunearyou/test_config_flow.py | 6 +- .../forked_daapd/test_config_flow.py | 24 +-- tests/components/foscam/test_config_flow.py | 24 +-- tests/components/freebox/test_config_flow.py | 20 +-- .../components/freedompro/test_config_flow.py | 4 +- tests/components/gdacs/test_config_flow.py | 8 +- tests/components/generic/test_config_flow.py | 52 +++--- tests/components/geofency/test_init.py | 4 +- .../geonetnz_quakes/test_config_flow.py | 8 +- .../geonetnz_volcano/test_config_flow.py | 6 +- tests/components/gios/test_config_flow.py | 4 +- tests/components/glances/test_config_flow.py | 6 +- tests/components/goalzero/test_config_flow.py | 22 +-- .../google_travel_time/test_config_flow.py | 24 +-- tests/components/gpslogger/test_init.py | 4 +- tests/components/gree/test_config_flow.py | 8 +- .../growatt_server/test_config_flow.py | 10 +- tests/components/guardian/test_config_flow.py | 24 +-- tests/components/hangouts/test_config_flow.py | 16 +- tests/components/harmony/test_config_flow.py | 4 +- tests/components/heos/test_config_flow.py | 14 +- .../here_travel_time/test_config_flow.py | 42 ++--- tests/components/hisense_aehw4a1/test_init.py | 4 +- tests/components/hive/test_config_flow.py | 74 ++++----- .../home_connect/test_config_flow.py | 2 +- .../home_plus_control/test_config_flow.py | 12 +- tests/components/homekit/test_config_flow.py | 154 +++++++++--------- .../components/honeywell/test_config_flow.py | 8 +- .../components/huawei_lte/test_config_flow.py | 16 +- .../components/huisbaasje/test_config_flow.py | 10 +- .../hvv_departures/test_config_flow.py | 8 +- tests/components/hyperion/test_config_flow.py | 82 +++++----- tests/components/ialarm/test_config_flow.py | 10 +- tests/components/icloud/test_config_flow.py | 34 ++-- tests/components/ifttt/test_init.py | 4 +- tests/components/insteon/test_config_flow.py | 38 ++--- tests/components/iqvia/test_config_flow.py | 8 +- .../islamic_prayer_times/test_config_flow.py | 10 +- tests/components/iss/test_config_flow.py | 8 +- tests/components/isy994/test_config_flow.py | 54 +++--- tests/components/izone/test_config_flow.py | 8 +- tests/components/jellyfin/test_config_flow.py | 2 +- .../keenetic_ndms2/test_config_flow.py | 26 +-- tests/components/kmtronic/test_config_flow.py | 4 +- .../launch_library/test_config_flow.py | 6 +- tests/components/lcn/test_config_flow.py | 6 +- tests/components/life360/test_config_flow.py | 18 +- tests/components/litejet/test_config_flow.py | 4 +- tests/components/local_ip/test_config_flow.py | 6 +- tests/components/locative/test_init.py | 4 +- .../logi_circle/test_config_flow.py | 20 +-- .../lutron_caseta/test_config_flow.py | 8 +- tests/components/lyric/test_config_flow.py | 8 +- tests/components/mailgun/test_init.py | 8 +- tests/components/mazda/test_config_flow.py | 36 ++-- tests/components/meater/test_config_flow.py | 10 +- .../met_eireann/test_config_flow.py | 6 +- .../meteo_france/test_config_flow.py | 22 +-- .../meteoclimatic/test_config_flow.py | 8 +- tests/components/mikrotik/test_config_flow.py | 12 +- .../modem_callerid/test_config_flow.py | 20 +-- .../components/monoprice/test_config_flow.py | 4 +- .../motion_blinds/test_config_flow.py | 4 +- .../components/motioneye/test_config_flow.py | 32 ++-- tests/components/mqtt/test_config_flow.py | 30 ++-- tests/components/nam/test_config_flow.py | 36 ++-- tests/components/neato/test_config_flow.py | 6 +- .../nest/test_config_flow_legacy.py | 32 ++-- tests/components/netatmo/test_config_flow.py | 24 +-- tests/components/netgear/test_config_flow.py | 32 ++-- tests/components/nextdns/test_config_flow.py | 8 +- .../nfandroidtv/test_config_flow.py | 8 +- .../components/nightscout/test_config_flow.py | 12 +- tests/components/nina/test_config_flow.py | 26 +-- .../nmap_tracker/test_config_flow.py | 4 +- tests/components/notion/test_config_flow.py | 10 +- tests/components/nuki/test_config_flow.py | 34 ++-- tests/components/nut/test_config_flow.py | 38 ++--- .../components/octoprint/test_config_flow.py | 8 +- .../components/omnilogic/test_config_flow.py | 4 +- .../components/ondilo_ico/test_config_flow.py | 2 +- tests/components/onvif/test_config_flow.py | 34 ++-- .../opentherm_gw/test_config_flow.py | 12 +- tests/components/openuv/test_config_flow.py | 12 +- .../openweathermap/test_config_flow.py | 12 +- tests/components/overkiz/test_config_flow.py | 20 +-- .../components/ovo_energy/test_config_flow.py | 26 +-- .../components/owntracks/test_config_flow.py | 12 +- .../components/philips_js/test_config_flow.py | 4 +- tests/components/picnic/test_config_flow.py | 18 +- tests/components/plaato/test_config_flow.py | 8 +- tests/components/point/test_config_flow.py | 24 +-- .../components/poolsense/test_config_flow.py | 4 +- tests/components/ps4/test_config_flow.py | 82 +++++----- tests/components/ps4/test_init.py | 4 +- .../pvpc_hourly_pricing/test_config_flow.py | 14 +- tests/components/qnap_qsw/test_config_flow.py | 12 +- .../components/radiotherm/test_config_flow.py | 22 +-- .../rainmachine/test_config_flow.py | 22 +-- .../recollect_waste/test_config_flow.py | 12 +- tests/components/renault/test_config_flow.py | 26 +-- tests/components/rfxtrx/test_config_flow.py | 16 +- tests/components/risco/test_config_flow.py | 8 +- tests/components/roomba/test_config_flow.py | 72 ++++---- tests/components/roon/test_config_flow.py | 2 +- tests/components/sabnzbd/test_config_flow.py | 4 +- tests/components/shelly/test_config_flow.py | 58 +++---- .../shopping_list/test_config_flow.py | 6 +- tests/components/sia/test_config_flow.py | 14 +- .../components/simplepush/test_config_flow.py | 12 +- .../components/simplisafe/test_config_flow.py | 56 +++---- tests/components/slack/test_config_flow.py | 16 +- tests/components/sleepiq/test_config_flow.py | 8 +- tests/components/smappee/test_config_flow.py | 64 ++++---- .../smartthings/test_config_flow.py | 102 ++++++------ tests/components/smarttub/test_config_flow.py | 8 +- .../components/solaredge/test_config_flow.py | 16 +- tests/components/solarlog/test_config_flow.py | 18 +- tests/components/soma/test_config_flow.py | 14 +- .../somfy_mylink/test_config_flow.py | 10 +- tests/components/sonos/test_init.py | 4 +- .../speedtestdotnet/test_config_flow.py | 16 +- tests/components/spider/test_config_flow.py | 10 +- tests/components/spotify/test_config_flow.py | 14 +- .../components/srp_energy/test_config_flow.py | 6 +- .../steam_online/test_config_flow.py | 38 ++--- .../components/syncthing/test_config_flow.py | 10 +- tests/components/syncthru/test_config_flow.py | 12 +- .../synology_dsm/test_config_flow.py | 46 +++--- tests/components/synology_dsm/test_init.py | 2 +- .../system_bridge/test_config_flow.py | 56 +++---- tests/components/tautulli/test_config_flow.py | 26 +-- .../tellduslive/test_config_flow.py | 36 ++-- tests/components/tile/test_config_flow.py | 14 +- .../components/tomorrowio/test_config_flow.py | 26 +-- tests/components/toon/test_config_flow.py | 18 +- .../totalconnect/test_config_flow.py | 24 +-- tests/components/traccar/test_init.py | 4 +- tests/components/tradfri/test_config_flow.py | 20 +-- .../transmission/test_config_flow.py | 20 +-- tests/components/twilio/test_init.py | 4 +- tests/components/unifi/test_config_flow.py | 42 ++--- tests/components/upcloud/test_config_flow.py | 10 +- tests/components/upnp/test_config_flow.py | 30 ++-- tests/components/velbus/test_config_flow.py | 20 +-- tests/components/vera/test_config_flow.py | 4 +- tests/components/vesync/test_config_flow.py | 8 +- tests/components/vicare/test_config_flow.py | 14 +- tests/components/vilfo/test_config_flow.py | 14 +- tests/components/vizio/test_config_flow.py | 118 +++++++------- tests/components/vulcan/test_config_flow.py | 110 ++++++------- tests/components/wallbox/test_config_flow.py | 2 +- tests/components/watttime/test_config_flow.py | 4 +- .../waze_travel_time/test_config_flow.py | 22 +-- tests/components/wemo/test_config_flow.py | 2 +- tests/components/wiffi/test_config_flow.py | 4 +- tests/components/withings/common.py | 2 +- tests/components/wolflink/test_config_flow.py | 8 +- tests/components/ws66i/test_config_flow.py | 4 +- tests/components/xbox/test_config_flow.py | 2 +- .../xiaomi_miio/test_config_flow.py | 8 +- .../yamaha_musiccast/test_config_flow.py | 30 ++-- tests/components/yolink/test_config_flow.py | 10 +- tests/helpers/test_config_entry_flow.py | 52 +++--- .../helpers/test_config_entry_oauth2_flow.py | 32 ++-- .../helpers/test_helper_config_entry_flow.py | 26 +-- tests/test_config_entries.py | 60 +++---- tests/test_data_entry_flow.py | 34 ++-- 237 files changed, 2284 insertions(+), 2280 deletions(-) diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index 035433986d4..a182de6d01f 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -100,37 +100,37 @@ async def test_login(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("pin") == str result = await hass.auth.login_flow.async_configure( result["flow_id"], {"pin": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_code" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"pin": "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"].id == "mock-id" @@ -147,9 +147,9 @@ async def test_setup_flow(hass): flow = await auth_module.async_setup_flow("new-user") result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init({"pin": "abcdefg"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert auth_module._data[1]["user_id"] == "new-user" assert auth_module._data[1]["pin"] == "abcdefg" diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 1d08ad70cc8..e69f11155a5 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -133,25 +133,25 @@ async def test_login_flow_validates_mfa(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str @@ -170,7 +170,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" @@ -187,7 +187,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" @@ -195,7 +195,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "too_many_retry" # wait service call finished @@ -203,13 +203,13 @@ async def test_login_flow_validates_mfa(hass): # restart login result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str @@ -228,7 +228,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": MOCK_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"].id == "mock-id" @@ -243,14 +243,14 @@ async def test_setup_user_notify_service(hass): flow = await notify_auth_module.async_setup_flow("test-user") step = await flow.async_step_init() - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "init" schema = step["data_schema"] schema({"notify_service": "test2"}) with patch("pyotp.HOTP.at", return_value=MOCK_CODE): step = await flow.async_step_init({"notify_service": "test1"}) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "setup" # wait service call finished @@ -266,7 +266,7 @@ async def test_setup_user_notify_service(hass): with patch("pyotp.HOTP.at", return_value=MOCK_CODE_2): step = await flow.async_step_setup({"code": "invalid"}) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "setup" assert step["errors"]["base"] == "invalid_code" @@ -283,7 +283,7 @@ async def test_setup_user_notify_service(hass): with patch("pyotp.HOTP.verify", return_value=True): step = await flow.async_step_setup({"code": MOCK_CODE_2}) - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_include_exclude_config(hass): @@ -332,7 +332,7 @@ async def test_setup_user_no_notify_service(hass): flow = await notify_auth_module.async_setup_flow("test-user") step = await flow.async_step_init() - assert step["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step["type"] == data_entry_flow.FlowResultType.ABORT assert step["reason"] == "no_available_service" @@ -369,13 +369,13 @@ async def test_not_raise_exception_when_service_not_exist(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_error" # wait service call finished diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index 2e4aad98066..53374e2d015 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -93,24 +93,24 @@ async def test_login_flow_validates_mfa(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str @@ -118,7 +118,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" @@ -126,7 +126,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": MOCK_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"].id == "mock-id" diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index e437ca9e331..22169812156 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -114,18 +114,18 @@ async def test_login_flow_validates(provider): """Test login flow.""" flow = await provider.async_login_flow({}) result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init( {"username": "bad-user", "password": "bad-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "good-user", "password": "good-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "good-user" @@ -135,5 +135,5 @@ async def test_strip_username(provider): result = await flow.async_step_init( {"username": "\t\ngood-user ", "password": "good-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "good-user" diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 62093df7210..e255135a0bd 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -115,24 +115,24 @@ async def test_login_flow_validates(data, hass): ) flow = await provider.async_login_flow({}) result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init( {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "TEST-user ", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "test-USER", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "test-USER" @@ -216,24 +216,24 @@ async def test_legacy_login_flow_validates(legacy_data, hass): ) flow = await provider.async_login_flow({}) result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init( {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "test-user" diff --git a/tests/auth/providers/test_legacy_api_password.py b/tests/auth/providers/test_legacy_api_password.py index c77cb676a6b..19af0a1e746 100644 --- a/tests/auth/providers/test_legacy_api_password.py +++ b/tests/auth/providers/test_legacy_api_password.py @@ -55,15 +55,15 @@ async def test_verify_login(hass, provider): async def test_login_flow_works(hass, manager): """Test wrong config.""" result = await manager.login_flow.async_init(handler=("legacy_api_password", None)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await manager.login_flow.async_configure( flow_id=result["flow_id"], user_input={"password": "not-hello"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await manager.login_flow.async_configure( flow_id=result["flow_id"], user_input={"password": "test-password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 22d720da587..49c2c776684 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -168,12 +168,12 @@ async def test_create_new_user(hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY credential = step["result"] assert credential is not None @@ -237,12 +237,12 @@ async def test_login_as_existing_user(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY credential = step["result"] user = await manager.async_get_user_by_credentials(credential) @@ -728,14 +728,14 @@ async def test_login_with_auth_module(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) # After auth_provider validated, request auth module input form - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" step = await manager.login_flow.async_configure( @@ -743,7 +743,7 @@ async def test_login_with_auth_module(mock_hass): ) # Invalid code error - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" assert step["errors"] == {"base": "invalid_code"} @@ -752,7 +752,7 @@ async def test_login_with_auth_module(mock_hass): ) # Finally passed, get credential - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert step["result"] assert step["result"].id == "mock-id" @@ -803,21 +803,21 @@ async def test_login_with_multi_auth_module(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) # After auth_provider validated, request select auth module - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "select_mfa_module" step = await manager.login_flow.async_configure( step["flow_id"], {"multi_factor_auth_module": "module2"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" step = await manager.login_flow.async_configure( @@ -825,7 +825,7 @@ async def test_login_with_multi_auth_module(mock_hass): ) # Finally passed, get credential - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert step["result"] assert step["result"].id == "mock-id" @@ -871,13 +871,13 @@ async def test_auth_module_expired_session(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" with patch( @@ -888,7 +888,7 @@ async def test_auth_module_expired_session(mock_hass): step["flow_id"], {"pin": "test-pin"} ) # login flow abort due session timeout - assert step["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step["type"] == data_entry_flow.FlowResultType.ABORT assert step["reason"] == "login_expired" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index adbea237d34..987a0b74996 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -23,7 +23,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -39,7 +39,7 @@ async def test_one_config_allowed(hass: HomeAssistant) -> None: step_user_result = await flow.async_step_user() - assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step_user_result["type"] == data_entry_flow.FlowResultType.ABORT assert step_user_result["reason"] == "single_instance_allowed" @@ -104,7 +104,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -125,7 +125,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" with patch( @@ -147,7 +147,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: result["flow_id"], user_input={"mfa_code": "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -175,7 +175,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: data=conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch("homeassistant.config_entries.ConfigEntries.async_reload"): @@ -184,7 +184,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: user_input=conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 32bb8bf7c70..9ed2fc82595 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -75,7 +75,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: ), ), patch( "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", - return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + return_value={"type": data_entry_flow.FlowResultType.FORM}, ) as mock_async_step_reauth: await setup_platform(hass, ALARM_DOMAIN) diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index c8f2d3c8c89..b4d804dceae 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -25,7 +25,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -134,7 +134,7 @@ async def test_create_entry(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "abcd" assert result["data"][CONF_NAME] == "abcd" assert result["data"][CONF_LATITUDE] == 55.55 @@ -171,14 +171,14 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FORECAST: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FORECAST: True} await hass.async_block_till_done() diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index c50e0b9971f..4990d196a08 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -47,7 +47,7 @@ async def test_show_form_no_hubs(hass, mock_hub_discover): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" # Check we performed the discovery @@ -66,7 +66,7 @@ async def test_show_form_one_hub(hass, mock_hub_discover, mock_hub_run): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_1.id assert result["result"].data == { CONF_HOST: DUMMY_HOST1, @@ -91,7 +91,7 @@ async def test_show_form_two_hubs(hass, mock_hub_discover): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Check we performed the discovery @@ -117,7 +117,7 @@ async def test_create_second_entry(hass, mock_hub_run, mock_hub_discover): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_2.id assert result["result"].data == { CONF_HOST: DUMMY_HOST2, diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index f5b30308a52..4bcfb60e7b6 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -35,7 +35,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_connection_error( ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -78,14 +78,14 @@ async def test_full_flow_implementation( assert result assert result.get("flow_id") - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=FIXTURE_USER_INPUT ) assert result2 - assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2.get("title") == FIXTURE_USER_INPUT[CONF_HOST] data = result2.get("data") @@ -132,7 +132,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -154,7 +154,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -176,14 +176,14 @@ async def test_hassio_confirm( context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "AdGuard Home Addon"} result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2 - assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2.get("title") == "AdGuard Home Addon" data = result2.get("data") @@ -215,6 +215,6 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index a8e219fff89..533cf2a17a4 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass, aioclient_mock): result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["type"] == data_entry_flow.FlowResultType.FORM assert result1["step_id"] == "user" assert result1["errors"] == {} @@ -33,7 +33,7 @@ async def test_form(hass, aioclient_mock): ) assert len(aioclient_mock.mock_calls) == 1 - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "testname" assert result2["data"] == USER_INPUT await hass.async_block_till_done() @@ -47,7 +47,7 @@ async def test_form(hass, aioclient_mock): result3["flow_id"], USER_INPUT, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result4["type"] == data_entry_flow.FlowResultType.ABORT async def test_form_cannot_connect(hass, aioclient_mock): @@ -66,7 +66,7 @@ async def test_form_cannot_connect(hass, aioclient_mock): USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index af14c170f40..bba177b96d2 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -49,7 +49,7 @@ async def test_form(hass): entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -79,14 +79,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_STATION_UPDATES: False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { CONF_STATION_UPDATES: False, } @@ -97,14 +97,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_STATION_UPDATES: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { CONF_STATION_UPDATES: True, } diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py index 01cb31b3f19..b36044e45b1 100644 --- a/tests/components/agent_dvr/test_config_flow.py +++ b/tests/components/agent_dvr/test_config_flow.py @@ -20,7 +20,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_user_device_exists_abort( @@ -35,7 +35,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "example.local", CONF_PORT: 8090}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_connection_error( @@ -53,7 +53,7 @@ async def test_connection_error( assert result["errors"]["base"] == "cannot_connect" assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_full_user_flow_implementation( @@ -78,7 +78,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "example.local", CONF_PORT: 8090} @@ -88,7 +88,7 @@ async def test_full_user_flow_implementation( assert result["data"][CONF_PORT] == 8090 assert result["data"][SERVER_URL] == "http://example.local:8090/" assert result["title"] == "DESKTOP" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "c0715bba-c2d0-48ef-9e3e-bc81c9ea4447" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 8b593e85cf4..b7c2a65812e 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -26,7 +26,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -84,7 +84,7 @@ async def test_create_entry(hass, aioclient_mock): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -106,7 +106,7 @@ async def test_create_entry_with_nearest_method(hass, aioclient_mock): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index b26775d7051..02236e826e5 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -72,7 +72,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch("pyairnow.WebServiceAPI._get", return_value=MOCK_RESPONSE), patch( @@ -86,7 +86,7 @@ async def test_form(hass): await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index f0a75417487..12a9e67122f 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -66,7 +66,7 @@ async def test_duplicate_error(hass, config, config_entry, data): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -126,7 +126,7 @@ async def test_errors(hass, data, exc, errors, integration_type): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == errors @@ -183,14 +183,14 @@ async def test_options_flow(hass, config_entry): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} @@ -205,7 +205,7 @@ async def test_step_geography_by_coords(hass, config, setup_airvisual): result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API (51.528308, -0.3817765)" assert result["data"] == { CONF_API_KEY: "abcde12345", @@ -237,7 +237,7 @@ async def test_step_geography_by_name(hass, config, setup_airvisual): result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API (Beijing, Beijing, China)" assert result["data"] == { CONF_API_KEY: "abcde12345", @@ -265,7 +265,7 @@ async def test_step_node_pro(hass, config, setup_airvisual): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Node/Pro (192.168.1.100)" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -282,7 +282,7 @@ async def test_step_reauth(hass, config_entry, setup_airvisual): assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_api_key = "defgh67890" @@ -293,7 +293,7 @@ async def test_step_reauth(hass, config_entry, setup_airvisual): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: new_api_key} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -306,7 +306,7 @@ async def test_step_user(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -315,7 +315,7 @@ async def test_step_user(hass): data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "geography_by_coords" result = await hass.config_entries.flow.async_init( @@ -324,7 +324,7 @@ async def test_step_user(hass): data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "geography_by_name" result = await hass.config_entries.flow.async_init( @@ -333,5 +333,5 @@ async def test_step_user(hass): data={"type": INTEGRATION_TYPE_NODE_PRO}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "node_pro" diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index 251dcf01b60..32eaade93ee 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -55,7 +55,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}" assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] assert result["data"][CONF_PORT] == CONFIG[CONF_PORT] @@ -84,7 +84,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {CONF_ID: "invalid_system_id"} @@ -95,7 +95,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: result["flow_id"], CONFIG_ID1 ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -103,7 +103,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result["title"] == f"Airzone {CONFIG_ID1[CONF_HOST]}:{CONFIG_ID1[CONF_PORT]}" diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index 8a2aae48f9b..e3fdb05ca00 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -62,7 +62,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -70,7 +70,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title): {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" with patch("homeassistant.components.alarmdecoder.config_flow.AdExt.open"), patch( @@ -82,7 +82,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == { **connection, @@ -105,7 +105,7 @@ async def test_setup_connection_error(hass: HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -113,7 +113,7 @@ async def test_setup_connection_error(hass: HomeAssistant): {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" with patch( @@ -123,7 +123,7 @@ async def test_setup_connection_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -133,7 +133,7 @@ async def test_setup_connection_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -152,7 +152,7 @@ async def test_options_arm_flow(hass: HomeAssistant): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -160,7 +160,7 @@ async def test_options_arm_flow(hass: HomeAssistant): user_input={"edit_selection": "Arming Settings"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "arm_settings" with patch( @@ -171,7 +171,7 @@ async def test_options_arm_flow(hass: HomeAssistant): user_input=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: user_input, OPTIONS_ZONES: DEFAULT_ZONE_OPTIONS, @@ -190,7 +190,7 @@ async def test_options_zone_flow(hass: HomeAssistant): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -198,7 +198,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -214,7 +214,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input=zone_settings, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {zone_number: zone_settings}, @@ -223,7 +223,7 @@ async def test_options_zone_flow(hass: HomeAssistant): # Make sure zone can be removed... result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -231,7 +231,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -247,7 +247,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {}, @@ -266,7 +266,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -274,7 +274,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" # Zone Number must be int @@ -283,7 +283,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={CONF_ZONE_NUMBER: "asd"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" assert result["errors"] == {CONF_ZONE_NUMBER: "int"} @@ -292,7 +292,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={CONF_ZONE_NUMBER: zone_number}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive @@ -301,7 +301,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_RELAY_ADDR: "1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -310,7 +310,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_RELAY_CHAN: "1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -320,7 +320,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_RELAY_ADDR: "abc", CONF_RELAY_CHAN: "abc"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == { CONF_RELAY_ADDR: "int", @@ -333,7 +333,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_ZONE_LOOP: "1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_rfid"} @@ -343,7 +343,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "ab"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "int"} @@ -353,7 +353,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "5"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_range"} @@ -372,7 +372,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: { @@ -420,7 +420,7 @@ async def test_one_device_allowed(hass, protocol, connection): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -428,11 +428,11 @@ async def test_one_device_allowed(hass, protocol, connection): {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index 32f49cff043..3bf2db14b95 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -57,7 +57,7 @@ async def test_hassio(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "hassio_confirm" with patch( @@ -67,7 +67,7 @@ async def test_hassio(hass): assert len(mock_setup.mock_calls) == 1 - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.async_entries(DOMAIN)) == 1 entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -83,15 +83,15 @@ async def test_abort_if_existing_entry(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await flow.async_step_import({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await flow.async_step_hassio(HassioServiceInfo(config={})) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -123,7 +123,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://almond.stanford.edu/me/api/oauth2/authorize" f"?response_type=code&client_id={CLIENT_ID_VALUE}" diff --git a/tests/components/amberelectric/test_config_flow.py b/tests/components/amberelectric/test_config_flow.py index ce474be1b3d..2be77f19bf1 100644 --- a/tests/components/amberelectric/test_config_flow.py +++ b/tests/components/amberelectric/test_config_flow.py @@ -67,7 +67,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: initial_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert initial_result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM assert initial_result.get("step_id") == "user" # Test filling in API key @@ -76,7 +76,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: API_KEY}, ) - assert enter_api_key_result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM assert enter_api_key_result.get("step_id") == "site" select_site_result = await hass.config_entries.flow.async_configure( @@ -85,7 +85,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: ) # Show available sites - assert select_site_result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert select_site_result.get("title") == "Home" data = select_site_result.get("data") assert data @@ -102,7 +102,7 @@ async def test_no_site(hass: HomeAssistant, no_site_api: Mock) -> None: data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "no_site"} @@ -113,7 +113,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -122,7 +122,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "invalid_api_token"} @@ -133,7 +133,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -142,7 +142,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "unknown_error"} diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 2da550afd42..e2c8e46dc46 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -35,7 +35,7 @@ async def test_abort_if_no_implementation_registered(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -48,12 +48,12 @@ async def test_abort_if_already_setup(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -63,7 +63,7 @@ async def test_full_flow_implementation(hass): flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert ( result["description_placeholders"]["cb_url"] @@ -78,7 +78,7 @@ async def test_full_flow_implementation(hass): with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value="test"): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Ambiclimate" assert result["data"]["callback_url"] == "https://example.com/api/ambiclimate" assert result["data"][CONF_CLIENT_SECRET] == "secret" @@ -86,14 +86,14 @@ async def test_full_flow_implementation(hass): with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT with patch( "ambiclimate.AmbiclimateOAuth.get_access_token", side_effect=ambiclimate.AmbiclimateOauthError(), ): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_abort_invalid_code(hass): @@ -103,7 +103,7 @@ async def test_abort_invalid_code(hass): with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("invalid") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "access_token" @@ -115,7 +115,7 @@ async def test_already_setup(hass): context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index a72534b8478..0e298c40c0e 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -15,7 +15,7 @@ async def test_duplicate_error(hass, config, config_entry, setup_ambient_station result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -31,7 +31,7 @@ async def test_errors(hass, config, devices, error, setup_ambient_station): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error} @@ -40,7 +40,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -49,7 +49,7 @@ async def test_step_user(hass, config, setup_ambient_station): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "67890fghij67" assert result["data"] == { CONF_API_KEY: "12345abcde12345abcde", diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index d5301f7ada3..a0fb86eb803 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -97,7 +97,7 @@ async def test_user(hass, config, eth_mac, wifi_mac): flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow_result["type"] == data_entry_flow.FlowResultType.FORM assert flow_result["step_id"] == "user" # test with all provided @@ -110,7 +110,7 @@ async def test_user(hass, config, eth_mac, wifi_mac): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config @@ -134,7 +134,7 @@ async def test_user_adbkey(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config_data @@ -152,7 +152,7 @@ async def test_error_both_key_server(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "key_and_server"} with patch( @@ -164,7 +164,7 @@ async def test_error_both_key_server(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -179,7 +179,7 @@ async def test_error_invalid_key(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "adbkey_not_file"} with patch( @@ -191,7 +191,7 @@ async def test_error_invalid_key(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -219,7 +219,7 @@ async def test_invalid_mac(hass, config, eth_mac, wifi_mac): data=config, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -237,7 +237,7 @@ async def test_abort_if_host_exist(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -260,7 +260,7 @@ async def test_abort_if_unique_exist(hass): data=CONFIG_ADB_SERVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -275,7 +275,7 @@ async def test_on_connect_failed(hass): result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -285,7 +285,7 @@ async def test_on_connect_failed(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} with patch( @@ -297,7 +297,7 @@ async def test_on_connect_failed(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == HOST assert result3["data"] == CONFIG_ADB_SERVER @@ -320,7 +320,7 @@ async def test_options_flow(hass): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test app form with existing app @@ -330,7 +330,7 @@ async def test_options_flow(hass): CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "apps" # test change value in apps form @@ -340,7 +340,7 @@ async def test_options_flow(hass): CONF_APP_NAME: "Appl1", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test app form with new app @@ -350,7 +350,7 @@ async def test_options_flow(hass): CONF_APPS: APPS_NEW_ID, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "apps" # test save value for new app @@ -361,7 +361,7 @@ async def test_options_flow(hass): CONF_APP_NAME: "Appl2", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test app form for delete @@ -371,7 +371,7 @@ async def test_options_flow(hass): CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "apps" # test delete app1 @@ -382,7 +382,7 @@ async def test_options_flow(hass): CONF_APP_DELETE: True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test rules form with existing rule @@ -392,7 +392,7 @@ async def test_options_flow(hass): CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" # test change value in rule form with invalid json rule @@ -402,7 +402,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: "a", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -413,7 +413,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: json.dumps({"a": "b"}), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -424,7 +424,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: json.dumps(["standby"]), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test rule form with new rule @@ -434,7 +434,7 @@ async def test_options_flow(hass): CONF_STATE_DETECTION_RULES: RULES_NEW_ID, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" # test save value for new rule @@ -445,7 +445,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: json.dumps(VALID_DETECT_RULE), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test rules form with delete existing rule @@ -455,7 +455,7 @@ async def test_options_flow(hass): CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" # test delete rule @@ -465,7 +465,7 @@ async def test_options_flow(hass): CONF_RULE_DELETE: True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -479,7 +479,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY apps_options = config_entry.options[CONF_APPS] assert apps_options.get("app1") is None diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 6efb4820564..862019b529e 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -71,7 +71,7 @@ async def test_user_input_device_not_found(hass, mrp_device): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -79,7 +79,7 @@ async def test_user_input_device_not_found(hass, mrp_device): {"device_input": "none"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "no_devices_found"} @@ -95,7 +95,7 @@ async def test_user_input_unexpected_error(hass, mock_scan): {"device_input": "dummy"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -104,31 +104,31 @@ async def test_user_adds_full_device(hass, full_device, pairing): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"device_input": "MRP Device"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["description_placeholders"] == {"protocol": "MRP"} result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["description_placeholders"] == {"protocol": "DMAP", "pin": 1111} result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result5["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result5["type"] == data_entry_flow.FlowResultType.FORM assert result5["description_placeholders"] == {"protocol": "AirPlay"} result6 = await hass.config_entries.flow.async_configure( @@ -157,14 +157,14 @@ async def test_user_adds_dmap_device(hass, dmap_device, dmap_pin, pairing): result["flow_id"], {"device_input": "DMAP Device"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["description_placeholders"] == {"pin": 1111, "protocol": "DMAP"} result6 = await hass.config_entries.flow.async_configure( @@ -195,7 +195,7 @@ async def test_user_adds_dmap_device_failed(hass, dmap_device, dmap_pin, pairing await hass.config_entries.flow.async_configure(result["flow_id"], {}) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "device_did_not_pair" @@ -211,7 +211,7 @@ async def test_user_adds_device_with_ip_filter( result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -268,7 +268,7 @@ async def test_user_adds_existing_device(hass, mrp_device): result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} @@ -294,7 +294,7 @@ async def test_user_connection_failed(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -315,7 +315,7 @@ async def test_user_start_pair_error_failed(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "invalid_auth" @@ -336,14 +336,14 @@ async def test_user_pair_service_with_password( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "password" result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "setup_failed" @@ -363,14 +363,14 @@ async def test_user_pair_disabled_service(hass, dmap_with_requirement, pairing_m result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol_disabled" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -390,7 +390,7 @@ async def test_user_pair_ignore_unsupported(hass, dmap_with_requirement, pairing result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "setup_failed" @@ -416,7 +416,7 @@ async def test_user_pair_invalid_pin(hass, mrp_device, pairing_mock): result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -442,7 +442,7 @@ async def test_user_pair_unexpected_error(hass, mrp_device, pairing_mock): result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -463,7 +463,7 @@ async def test_user_pair_backoff_error(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "backoff" @@ -484,7 +484,7 @@ async def test_user_pair_begin_unexpected_error(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "unknown" @@ -499,14 +499,14 @@ async def test_ignores_disabled_service(hass, airplay_with_disabled_mrp, pairing result["flow_id"], {"device_input": "mrpid"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == { "name": "AirPlay Device", "type": "Unknown", } result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "AirPlay"} result3 = await hass.config_entries.flow.async_configure( @@ -541,7 +541,7 @@ async def test_zeroconf_unsupported_service_aborts(hass): properties={}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" @@ -560,7 +560,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): type="_mediaremotetv._tcp.local.", ), ) - assert unrelated_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert unrelated_result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, @@ -575,7 +575,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): type="_mediaremotetv._tcp.local.", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", @@ -585,7 +585,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( @@ -605,7 +605,7 @@ async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -615,7 +615,7 @@ async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "DMAP", "pin": 1111} result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -654,7 +654,7 @@ async def test_zeroconf_ip_change(hass, mock_scan): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -693,7 +693,7 @@ async def test_zeroconf_ip_change_via_secondary_identifier(hass, mock_scan): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -709,7 +709,7 @@ async def test_zeroconf_add_existing_aborts(hass, dmap_device): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -718,7 +718,7 @@ async def test_zeroconf_add_but_device_not_found(hass, mock_scan): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -729,7 +729,7 @@ async def test_zeroconf_add_existing_device(hass, dmap_device): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -740,7 +740,7 @@ async def test_zeroconf_unexpected_error(hass, mock_scan): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" @@ -764,7 +764,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" mock_scan.result = [ @@ -786,7 +786,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, ), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -844,7 +844,7 @@ async def test_zeroconf_missing_device_during_protocol_resolve( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "device_not_found" @@ -904,7 +904,7 @@ async def test_zeroconf_additional_protocol_resolve_failure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "inconsistent_device" @@ -930,7 +930,7 @@ async def test_zeroconf_pair_additionally_found_protocols( properties={"deviceid": "airplayid"}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM await hass.async_block_till_done() mock_scan.result = [ @@ -981,7 +981,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pair_no_pin" assert result2["description_placeholders"] == {"pin": ANY, "protocol": "RAOP"} @@ -991,7 +991,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "pair_with_pin" assert result3["description_placeholders"] == {"protocol": "MRP"} @@ -999,7 +999,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["step_id"] == "pair_with_pin" assert result4["description_placeholders"] == {"protocol": "AirPlay"} @@ -1007,7 +1007,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result5["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Re-configuration @@ -1030,13 +1030,13 @@ async def test_reconfigure_update_credentials(hass, mrp_device, pairing): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert config_entry.data == { @@ -1058,7 +1058,7 @@ async def test_option_start_off(hass): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_START_OFF: True} @@ -1083,5 +1083,5 @@ async def test_zeroconf_rejects_ipv6(hass): properties={"CtlN": "Apple TV"}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index dd5995a8f4f..3b7a414305f 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -160,7 +160,7 @@ class OAuthFixture: ) result = await self.hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == self.title assert "data" in result assert "token" in result["data"] @@ -417,7 +417,7 @@ async def test_config_flow_no_credentials(hass): result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -444,7 +444,7 @@ async def test_config_flow_other_domain( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -467,7 +467,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP result = await oauth_fixture.complete_external_step(result) assert ( result["data"].get("auth_implementation") == "fake_integration_some_client_id" @@ -511,14 +511,14 @@ async def test_config_flow_multiple_entries( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "pick_implementation" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"implementation": "fake_integration_some_client_id2"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.client_id = CLIENT_ID + "2" oauth_fixture.title = CLIENT_ID + "2" result = await oauth_fixture.complete_external_step(result) @@ -548,7 +548,7 @@ async def test_config_flow_create_delete_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -565,7 +565,7 @@ async def test_config_flow_with_config_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -583,7 +583,7 @@ async def test_import_without_setup(hass, config_credential): result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -612,7 +612,7 @@ async def test_websocket_without_platform( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -687,7 +687,7 @@ async def test_platform_with_auth_implementation( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -745,7 +745,7 @@ async def test_name( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.title = NAME result = await oauth_fixture.complete_external_step(result) assert ( diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index efcbb0b691c..eeb5adbc7e3 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -65,11 +65,11 @@ async def test_ssdp(hass, dummy_client): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY @@ -86,7 +86,7 @@ async def test_ssdp_abort(hass): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -99,11 +99,11 @@ async def test_ssdp_unable_to_connect(hass, dummy_client): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -122,7 +122,7 @@ async def test_ssdp_update(hass): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == MOCK_HOST @@ -137,7 +137,7 @@ async def test_user(hass, aioclient_mock): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -149,7 +149,7 @@ async def test_user(hass, aioclient_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id == MOCK_UUID @@ -168,7 +168,7 @@ async def test_invalid_ssdp(hass, aioclient_mock): context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id is None @@ -187,7 +187,7 @@ async def test_user_wrong(hass, aioclient_mock): context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["result"].unique_id is None diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 8475bd48f9a..013a81e7184 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -81,7 +81,7 @@ async def test_user(hass, mock_unique_id, unique_id): flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow_result["type"] == data_entry_flow.FlowResultType.FORM assert flow_result["step_id"] == "user" # test with all provided @@ -92,7 +92,7 @@ async def test_user(hass, mock_unique_id, unique_id): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA @@ -116,7 +116,7 @@ async def test_error_wrong_password_ssh(hass, config, error): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error} @@ -136,7 +136,7 @@ async def test_error_invalid_ssh(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "ssh_not_file"} @@ -152,7 +152,7 @@ async def test_error_invalid_host(hass): data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_host"} @@ -168,7 +168,7 @@ async def test_abort_if_not_unique_id_setup(hass): context={"source": SOURCE_USER}, data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_unique_id" @@ -192,7 +192,7 @@ async def test_update_uniqueid_exist(hass, mock_unique_id): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA prev_entry = hass.config_entries.async_get_entry(existing_entry.entry_id) @@ -214,7 +214,7 @@ async def test_abort_invalid_unique_id(hass): context={"source": SOURCE_USER}, data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -244,7 +244,7 @@ async def test_on_connect_failed(hass, side_effect, error): result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error} @@ -262,7 +262,7 @@ async def test_options_flow(hass): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -276,7 +276,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_CONSIDER_HOME] == 20 assert config_entry.options[CONF_TRACK_UNKNOWN] is True assert config_entry.options[CONF_INTERFACE] == "aaa" diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index a92e73ae18e..9eeddff9a45 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -17,7 +17,7 @@ async def test_show_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocke DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -30,7 +30,7 @@ async def test_adding_second_device( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" with patch( "pyatag.AtagOne.id", @@ -39,7 +39,7 @@ async def test_adding_second_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_connection_error( @@ -53,7 +53,7 @@ async def test_connection_error( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -66,7 +66,7 @@ async def test_unauthorized(hass: HomeAssistant, aioclient_mock: AiohttpClientMo context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unauthorized"} @@ -81,6 +81,6 @@ async def test_full_flow_implementation( context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == UID assert result["result"].unique_id == UID diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index 9e83810978a..f106b19cea9 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -101,7 +101,7 @@ async def test_option_flow(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -109,6 +109,6 @@ async def test_option_flow(hass): user_input={"forecast_threshold": 65}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"]["forecast_threshold"] == 65 diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index b30d6dc5eeb..73b4b67bc04 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -58,7 +58,7 @@ async def test_form(hass): {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_PORT: "/dev/ttyUSB7", diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index 9e2e0b7cccc..3eb1972011c 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -23,7 +23,7 @@ async def test_auth_failure(hass: HomeAssistant) -> None: """Test init with an authentication failure.""" with patch( "homeassistant.components.aussie_broadband.config_flow.ConfigFlow.async_step_reauth", - return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + return_value={"type": data_entry_flow.FlowResultType.FORM}, ) as mock_async_step_reauth: await setup_platform(hass, side_effect=AuthenticationException()) mock_async_step_reauth.assert_called_once() diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index edf45742dbd..b0851a3cfe6 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -64,7 +64,7 @@ async def test_ws_setup_depose_mfa(hass, hass_ws_client): assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow["type"] == data_entry_flow.FlowResultType.FORM assert flow["handler"] == "example_module" assert flow["step_id"] == "init" assert flow["data_schema"][0] == {"type": "string", "name": "pin", "required": True} @@ -83,7 +83,7 @@ async def test_ws_setup_depose_mfa(hass, hass_ws_client): assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert flow["handler"] == "example_module" assert flow["data"]["result"] is None diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 47e58fea421..b5bc5f23eaa 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -21,7 +21,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -95,7 +95,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -105,7 +105,7 @@ async def test_reauth(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} @@ -117,7 +117,7 @@ async def test_reauth(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -133,7 +133,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -143,7 +143,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" @@ -160,7 +160,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "foo@bar.com (32406)" assert result["data"][CONF_ACCESS_TOKEN] == CONFIG[CONF_ACCESS_TOKEN] assert result["result"].unique_id == UNIQUE_ID diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index 7817f6fc570..713d3f31a2f 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -27,7 +27,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -41,7 +41,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -50,7 +50,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -67,7 +67,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +76,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "invalid_auth"} @@ -91,7 +91,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -100,7 +100,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -117,7 +117,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -126,7 +126,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "cannot_connect"} @@ -146,7 +146,7 @@ async def test_project_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -155,7 +155,7 @@ async def test_project_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "project_error"} @@ -177,7 +177,7 @@ async def test_reauth_project_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -186,7 +186,7 @@ async def test_reauth_project_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "project_error"} @@ -208,7 +208,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" assert result["errors"] == {"base": "invalid_auth"} @@ -229,7 +229,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -253,7 +253,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -263,7 +263,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result2["title"] == f"{FIXTURE_USER_INPUT[CONF_ORG]}/{FIXTURE_USER_INPUT[CONF_PROJECT]}" diff --git a/tests/components/azure_event_hub/test_config_flow.py b/tests/components/azure_event_hub/test_config_flow.py index 4e135d55555..93615561289 100644 --- a/tests/components/azure_event_hub/test_config_flow.py +++ b/tests/components/azure_event_hub/test_config_flow.py @@ -63,7 +63,7 @@ async def test_form( result2["flow_id"], step2_config.copy(), ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "test-instance" assert result3["data"] == data_config mock_setup_entry.assert_called_once() @@ -79,7 +79,7 @@ async def test_import(hass, mock_setup_entry): data=IMPORT_CONFIG.copy(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test-instance" options = { CONF_SEND_INTERVAL: import_config.pop(CONF_SEND_INTERVAL), @@ -109,7 +109,7 @@ async def test_single_instance(hass, source): context={"source": source}, data=BASE_CONFIG_CS.copy(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -138,7 +138,7 @@ async def test_connection_error_sas( result["flow_id"], SAS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -168,7 +168,7 @@ async def test_connection_error_cs( result["flow_id"], CS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -176,13 +176,13 @@ async def test_options_flow(hass, entry): """Test options flow.""" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], UPDATE_OPTIONS ) - assert updated["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert updated["data"] == UPDATE_OPTIONS await hass.async_block_till_done() diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py index ffb8c80a29e..2b448a3df56 100644 --- a/tests/components/balboa/test_config_flow.py +++ b/tests/components/balboa/test_config_flow.py @@ -124,7 +124,7 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -137,5 +137,5 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert dict(config_entry.options) == {CONF_SYNC_TIME: True} diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 3f40880abf7..7db216e294e 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -159,7 +159,7 @@ async def test_already_configured(hass, valid_feature_mock): context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "address_already_configured" diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 5ea03eb2b62..999204a2d91 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -250,7 +250,7 @@ async def test_reauth_shows_user_step(hass): context={"source": config_entries.SOURCE_REAUTH}, data={"username": "blink@example.com", "password": "invalid_password"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -280,7 +280,7 @@ async def test_options_flow(hass): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "simple_options" result = await hass.config_entries.options.async_configure( @@ -288,6 +288,6 @@ async def test_options_flow(hass): user_input={"scan_interval": 5}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {"scan_interval": 5} assert mock_blink.refresh_rate == 5 diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 10178c22de8..3f22f984a54 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -35,7 +35,7 @@ async def test_show_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_connection_error(hass): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -75,7 +75,7 @@ async def test_full_user_flow_implementation(hass): context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] assert result2["data"] == FIXTURE_COMPLETE_ENTRY @@ -98,7 +98,7 @@ async def test_options_flow_implementation(hass): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "account_options" result = await hass.config_entries.options.async_configure( @@ -107,7 +107,7 @@ async def test_options_flow_implementation(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_READ_ONLY: True, } diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 16d76c5d753..f61a8d312b2 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -38,7 +38,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -88,7 +88,7 @@ async def test_authorize_no_ip_control(hass): DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_ip_control" @@ -117,7 +117,7 @@ async def test_duplicate_error(hass): result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -135,14 +135,14 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -168,14 +168,14 @@ async def test_create_entry_with_ipv6_address(hass): data={CONF_HOST: "2001:db8::1428:57ab"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -214,7 +214,7 @@ async def test_options_flow(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( @@ -222,5 +222,5 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_IGNORED_SOURCES: ["HDMI 1", "HDMI 2"]} diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 00493011500..60f29dbd901 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -21,7 +21,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -37,7 +37,7 @@ async def test_create_entry_with_hostname(hass): data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "example.local" assert result["data"][CONF_TYPE] == "laser" @@ -53,7 +53,7 @@ async def test_create_entry_with_ipv4_address(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" @@ -71,7 +71,7 @@ async def test_create_entry_with_ipv6_address(hass): data={CONF_HOST: "2001:db8::1428:57ab", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "2001:db8::1428:57ab" assert result["data"][CONF_TYPE] == "laser" @@ -116,7 +116,7 @@ async def test_unsupported_model_error(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unsupported_model" @@ -133,7 +133,7 @@ async def test_device_exists_abort(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_zeroconf_snmp_error(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -176,7 +176,7 @@ async def test_zeroconf_unsupported_model(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unsupported_model" assert len(mock_get_data.mock_calls) == 0 @@ -208,7 +208,7 @@ async def test_zeroconf_device_exists_abort(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -235,7 +235,7 @@ async def test_zeroconf_no_probe_existing_device(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_get_data.mock_calls) == 0 @@ -264,13 +264,13 @@ async def test_zeroconf_confirm_create_entry(hass): assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"]["model"] == "HL-L2340DW" assert result["description_placeholders"]["serial_number"] == "0123456789" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TYPE: "laser"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py index 4a949911ca6..b2eea79d463 100644 --- a/tests/components/brunt/test_config_flow.py +++ b/tests/components/brunt/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -57,7 +57,7 @@ async def test_form_duplicate_login(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -80,17 +80,17 @@ async def test_form_error(hass, side_effect, error_message): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error_message} @pytest.mark.parametrize( "side_effect, result_type, password, step_id, reason", [ - (None, data_entry_flow.RESULT_TYPE_ABORT, "test", None, "reauth_successful"), + (None, data_entry_flow.FlowResultType.ABORT, "test", None, "reauth_successful"), ( Exception, - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, CONFIG[CONF_PASSWORD], "reauth_confirm", None, @@ -115,7 +115,7 @@ async def test_reauth(hass, side_effect, result_type, password, step_id, reason) }, data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index 38485fb7959..b8efa960fca 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -28,7 +28,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_connection_error( @@ -54,7 +54,7 @@ async def test_connection_error( assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_user_device_exists_abort( @@ -75,7 +75,7 @@ async def test_user_device_exists_abort( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_full_user_flow_implementation( @@ -94,7 +94,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -114,7 +114,7 @@ async def test_full_user_flow_implementation( assert result["data"][CONF_PORT] == 80 assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127" assert result["title"] == "RVS21.831F/127" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "RVS21.831F/127" @@ -136,7 +136,7 @@ async def test_full_user_flow_implementation_without_auth( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -153,7 +153,7 @@ async def test_full_user_flow_implementation_without_auth( assert result["data"][CONF_PORT] == 80 assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127" assert result["title"] == "RVS21.831F/127" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "RVS21.831F/127" diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py index 828101bf77e..420e89b5bc2 100644 --- a/tests/components/buienradar/test_config_flow.py +++ b/tests/components/buienradar/test_config_flow.py @@ -96,7 +96,7 @@ async def test_options_flow(hass): ), patch( "homeassistant.components.buienradar.async_unload_entry", return_value=True ): - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index d7aa0fdeda9..97218a396dd 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -24,10 +24,10 @@ async def test_creating_entry_sets_up_media_player(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_option_flow(hass, parameter_data): # Test ignore_cec and uuid options are hidden if advanced options are disabled result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema assert set(data_schema) == {"known_hosts"} @@ -201,7 +201,7 @@ async def test_option_flow(hass, parameter_data): result = await hass.config_entries.options.async_init( config_entry.entry_id, context=context ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema for other_param in basic_parameters: @@ -218,7 +218,7 @@ async def test_option_flow(hass, parameter_data): result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "advanced_options" for other_param in basic_parameters: if other_param == parameter: @@ -243,7 +243,7 @@ async def test_option_flow(hass, parameter_data): result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] is None for other_param in advanced_parameters: if other_param == parameter: @@ -257,7 +257,7 @@ async def test_option_flow(hass, parameter_data): result["flow_id"], user_input={"known_hosts": ""}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] is None expected_data = {**orig_data, "known_hosts": []} if parameter in advanced_parameters: diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index a1cd1367e5f..4c6409b56bd 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -18,7 +18,7 @@ async def test_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -27,7 +27,7 @@ async def test_user(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -42,7 +42,7 @@ async def test_user_with_bad_cert(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -53,7 +53,7 @@ async def test_user_with_bad_cert(hass): result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -78,7 +78,7 @@ async def test_import_host_only(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT @@ -100,7 +100,7 @@ async def test_import_host_and_port(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -122,7 +122,7 @@ async def test_import_non_default_port(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{HOST}:888" assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == 888 @@ -144,7 +144,7 @@ async def test_import_with_name(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -163,7 +163,7 @@ async def test_bad_import(hass): data={CONF_HOST: HOST}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "import_failed" @@ -180,7 +180,7 @@ async def test_abort_if_already_setup(hass): context={"source": config_entries.SOURCE_IMPORT}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -188,7 +188,7 @@ async def test_abort_if_already_setup(hass): context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -205,7 +205,7 @@ async def test_abort_on_socket_failed(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "resolve_failed"} with patch( @@ -215,7 +215,7 @@ async def test_abort_on_socket_failed(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_timeout"} with patch( @@ -225,5 +225,5 @@ async def test_abort_on_socket_failed(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_refused"} diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py index 9aa16b8a7c5..69bcf5f9819 100644 --- a/tests/components/climacell/test_config_flow.py +++ b/tests/components/climacell/test_config_flow.py @@ -33,14 +33,14 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMESTEP: 1} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMESTEP] == 1 assert entry.options[CONF_TIMESTEP] == 1 diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index c1022dc6aae..16b43d8a3c8 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -132,7 +132,7 @@ async def test_implementation(hass, flow_handler, current_request_with_host): TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == "http://example.com/auth" flow_finished.set_result( diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 611a7f75939..0de4bf44401 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1075,7 +1075,7 @@ async def test_ignore_flow(hass, hass_ws_client): result = await hass.config_entries.flow.async_init( "test", context={"source": core_ce.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM await ws_client.send_json( { diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index fdc0df108ee..edc026c9304 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -183,7 +183,7 @@ async def test_no_user_input(crownstone_setup: MockFixture, hass: HomeAssistant) DOMAIN, context={"source": "user"} ) # show the login form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert crownstone_setup.call_count == 0 @@ -211,7 +211,7 @@ async def test_abort_if_configured(crownstone_setup: MockFixture, hass: HomeAssi result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # test if we abort if we try to configure the same entry - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert crownstone_setup.call_count == 0 @@ -228,7 +228,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} # side effect: auth error account not verified @@ -238,7 +238,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "account_not_verified"} assert crownstone_setup.call_count == 0 @@ -251,7 +251,7 @@ async def test_unknown_error(crownstone_setup: MockFixture, hass: HomeAssistant) result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown_error"} assert crownstone_setup.call_count == 0 @@ -271,14 +271,14 @@ async def test_successful_login_no_usb( result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # should show usb form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" # don't setup USB dongle, create entry result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: DONT_USE_USB} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_without_usb assert result["options"] == entry_options_without_usb assert crownstone_setup.call_count == 1 @@ -304,7 +304,7 @@ async def test_successful_login_with_usb( hass, get_mocked_crownstone_cloud(create_mocked_spheres(2)) ) # should show usb form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports_none_types.call_count == 1 @@ -324,7 +324,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports_none_types.call_count == 2 assert usb_path.call_count == 1 @@ -333,7 +333,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_usb assert result["options"] == entry_options_with_usb assert crownstone_setup.call_count == 1 @@ -356,7 +356,7 @@ async def test_successful_login_with_manual_usb_path( hass, get_mocked_crownstone_cloud(create_mocked_spheres(1)) ) # should show usb form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -365,7 +365,7 @@ async def test_successful_login_with_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -377,7 +377,7 @@ async def test_successful_login_with_manual_usb_path( # since we only have 1 sphere here, test that it's automatically selected and # creating entry without asking for user input - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_manual_usb assert result["options"] == entry_options_with_manual_usb assert crownstone_setup.call_count == 1 @@ -413,7 +413,7 @@ async def test_options_flow_setup_usb( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -427,7 +427,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -447,7 +447,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports.call_count == 2 assert usb_path.call_count == 1 @@ -456,7 +456,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_1" ) @@ -490,7 +490,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -507,7 +507,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant): CONF_USB_SPHERE_OPTION: "sphere_name_0", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=None, usb_sphere=None ) @@ -543,13 +543,13 @@ async def test_options_flow_manual_usb_path( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -558,7 +558,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -568,7 +568,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_MANUAL_PATH: path} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=path, usb_sphere="sphere_id_0" ) @@ -602,14 +602,14 @@ async def test_options_flow_change_usb_sphere(hass: HomeAssistant): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True, CONF_USB_SPHERE_OPTION: "sphere_name_2"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_2" ) diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index 91197927e95..39127f972fe 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -425,7 +425,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -433,7 +433,7 @@ async def test_options_flow(hass): user_input={CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, CONF_ZONE3: True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index 48544ca0158..b20321277fb 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -33,7 +33,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIG[CONF_USERNAME] assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -54,7 +54,7 @@ async def test_form_account_error(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -73,7 +73,7 @@ async def test_form_session_error(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -92,7 +92,7 @@ async def test_form_unknown_error(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -107,14 +107,14 @@ async def test_option_flow_default(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_UNIT_OF_MEASUREMENT: MG_DL, } @@ -131,14 +131,14 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_UNIT_OF_MEASUREMENT: MMOL_L, } diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 5c4ef3e99f9..8bd185e8011 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -87,10 +87,10 @@ async def fixture(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "dialogflow", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] return await hass_client_no_auth(), webhook_id diff --git a/tests/components/discord/test_config_flow.py b/tests/components/discord/test_config_flow.py index 9d4966929be..b6504e851ca 100644 --- a/tests/components/discord/test_config_flow.py +++ b/tests/components/discord/test_config_flow.py @@ -28,7 +28,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -45,7 +45,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -58,7 +58,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -67,7 +67,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -81,7 +81,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -90,7 +90,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -104,7 +104,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -113,7 +113,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -131,7 +131,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_conf = {CONF_API_TOKEN: "1234567890123"} @@ -141,7 +141,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -150,6 +150,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA | new_conf diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 7ec25906f99..8035b7ee822 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -84,7 +84,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -92,7 +92,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -118,7 +118,7 @@ async def test_user_flow_discovered_manual( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -126,7 +126,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -134,7 +134,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -158,7 +158,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -166,7 +166,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_NAME} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -188,7 +188,7 @@ async def test_user_flow_uncontactable( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -196,7 +196,7 @@ async def test_user_flow_uncontactable( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "manual" @@ -221,7 +221,7 @@ async def test_user_flow_embedded_st( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -229,7 +229,7 @@ async def test_user_flow_embedded_st( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -251,7 +251,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -259,7 +259,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "not_dmr"} assert result["step_id"] == "manual" @@ -271,7 +271,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -279,7 +279,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -302,7 +302,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError @@ -312,7 +312,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -342,7 +342,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -357,7 +357,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -382,7 +382,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -398,7 +398,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" # Service list does not contain services @@ -410,7 +410,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" # AVTransport service is missing @@ -426,7 +426,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -448,7 +448,7 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -462,7 +462,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "alternative_integration" discovery = dataclasses.replace(MOCK_DISCOVERY) @@ -475,7 +475,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "alternative_integration" for manufacturer, model in [ @@ -493,7 +493,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "alternative_integration" @@ -507,7 +507,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == {} @@ -526,7 +526,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -534,7 +534,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -559,7 +559,7 @@ async def test_unignore_flow_offline( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == {} @@ -572,7 +572,7 @@ async def test_unignore_flow_offline( context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "discovery_error" @@ -583,7 +583,7 @@ async def test_options_flow( config_entry_mock.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry_mock.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -597,7 +597,7 @@ async def test_options_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_url"} @@ -612,7 +612,7 @@ async def test_options_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LISTEN_PORT: 2222, CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py index 591457b23c6..1d6ac0eaf80 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -86,7 +86,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -95,7 +95,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -119,7 +119,7 @@ async def test_user_flow_no_devices( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -130,7 +130,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -138,7 +138,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -161,7 +161,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" upnp_factory_mock.async_create_device.side_effect = UpnpError @@ -171,7 +171,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -201,7 +201,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -216,7 +216,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -230,7 +230,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "bad_ssdp" # Missing USN @@ -240,7 +240,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "bad_ssdp" @@ -280,7 +280,7 @@ async def test_duplicate_name( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -288,7 +288,7 @@ async def test_duplicate_name( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: new_device_location, @@ -318,7 +318,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -334,7 +334,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" # Service list does not contain services @@ -346,7 +346,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" # ContentDirectory service is missing @@ -362,7 +362,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" @@ -384,5 +384,5 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index cbf455653ff..f79a60e3265 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -43,7 +43,7 @@ async def test_user_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} doorbirdapi = _get_mock_doorbirdapi_return_values( @@ -188,7 +188,7 @@ async def test_form_zeroconf_correct_oui(hass): ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -251,7 +251,7 @@ async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_ ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_doorbird_device" @@ -271,7 +271,7 @@ async def test_form_user_cannot_connect(hass): VALID_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -312,12 +312,12 @@ async def test_options_flow(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_EVENTS: "eventa, eventc, eventq"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_EVENTS: ["eventa", "eventc", "eventq"]} diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index ddec7bda888..50fd1e4b7a8 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -562,7 +562,7 @@ async def test_options_flow(hass): with patch( "homeassistant.components.dsmr.async_setup_entry", return_value=True ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index 76585ac73a1..9cc32a40371 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -74,7 +74,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_HOSTNAME ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "dunehd-host" assert result["data"] == {CONF_HOST: "dunehd-host"} @@ -90,6 +90,6 @@ async def test_create_entry_with_ipv6_address(hass): data={CONF_HOST: "2001:db8::1428:57ab"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "2001:db8::1428:57ab" assert result["data"] == {CONF_HOST: "2001:db8::1428:57ab"} diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 8311b4aba2c..0c80f20859f 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -25,7 +25,7 @@ async def test_abort_if_already_setup(hass): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -36,7 +36,7 @@ async def test_user_step_without_user_input(hass): flow.hass.data[DATA_ECOBEE_CONFIG] = {} result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -53,7 +53,7 @@ async def test_pin_request_succeeds(hass): result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -70,7 +70,7 @@ async def test_pin_request_fails(hass): result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "pin_request_failed" @@ -92,7 +92,7 @@ async def test_token_request_succeeds(hass): result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", @@ -116,7 +116,7 @@ async def test_token_request_fails(hass): result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" assert result["errors"]["base"] == "token_request_failed" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -131,7 +131,7 @@ async def test_import_flow_triggered_but_no_ecobee_conf(hass): result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -155,7 +155,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_t result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py index 5b8d42799e9..e4e9889aadd 100644 --- a/tests/components/elmax/test_config_flow.py +++ b/tests/components/elmax/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -60,7 +60,7 @@ async def test_standard_setup(hass): }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_one_config_allowed(hass): @@ -94,7 +94,7 @@ async def test_one_config_allowed(hass): CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -115,7 +115,7 @@ async def test_invalid_credentials(hass): }, ) assert login_result["step_id"] == "user" - assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "invalid_auth"} @@ -136,7 +136,7 @@ async def test_connection_error(hass): }, ) assert login_result["step_id"] == "user" - assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "network_error"} @@ -164,7 +164,7 @@ async def test_unhandled_error(hass): }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -193,7 +193,7 @@ async def test_invalid_pin(hass): }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -215,7 +215,7 @@ async def test_no_online_panel(hass): }, ) assert login_result["step_id"] == "user" - assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "no_panel_online"} @@ -231,7 +231,7 @@ async def test_show_reauth(hass): CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -272,7 +272,7 @@ async def test_reauth_flow(hass): CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() assert result["reason"] == "reauth_successful" @@ -316,7 +316,7 @@ async def test_reauth_panel_disappeared(hass): }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "reauth_panel_disappeared"} @@ -358,7 +358,7 @@ async def test_reauth_invalid_pin(hass): }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -400,5 +400,5 @@ async def test_reauth_bad_login(hass): }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index d12b9a580c7..7585c2699cd 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user_flow_cannot_create_multiple_instances(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -37,7 +37,7 @@ async def test_user_flow_with_detected_dongle(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "detect" devices = result["data_schema"].schema.get("device").container assert FAKE_DONGLE_PATH in devices @@ -51,7 +51,7 @@ async def test_user_flow_with_no_detected_dongle(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "manual" @@ -64,7 +64,7 @@ async def test_detection_flow_with_valid_path(hass): DOMAIN, context={"source": "detect"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -82,7 +82,7 @@ async def test_detection_flow_with_custom_path(hass): data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "manual" @@ -100,7 +100,7 @@ async def test_detection_flow_with_invalid_path(hass): data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "detect" assert CONF_DEVICE in result["errors"] @@ -114,7 +114,7 @@ async def test_manual_flow_with_valid_path(hass): DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -130,7 +130,7 @@ async def test_manual_flow_with_invalid_path(hass): DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "manual" assert CONF_DEVICE in result["errors"] @@ -146,7 +146,7 @@ async def test_import_flow_with_valid_path(hass): data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == DATA_TO_IMPORT[CONF_DEVICE] @@ -164,5 +164,5 @@ async def test_import_flow_with_invalid_path(hass): data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_dongle_path" diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index de3ff516eae..9d484d984a8 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -64,7 +64,7 @@ async def test_create_entry(hass): flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE @@ -89,7 +89,7 @@ async def test_create_same_entry_twice(hass): flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -135,6 +135,6 @@ async def test_lat_lon_not_specified(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=fake_config ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index 2ab4aaf6dd6..8b869d4830e 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -56,7 +56,7 @@ async def test_duplicate_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index 0553574ae77..35467ffc449 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -42,7 +42,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_abort_if_already_setup(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -91,7 +91,7 @@ async def test_step_user(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_CONF[CONF_USERNAME] assert result["data"] == { "auth_implementation": "fireservicerota", @@ -131,7 +131,7 @@ async def test_reauth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.fireservicerota.config_flow.FireServiceRota" @@ -147,5 +147,5 @@ async def test_reauth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index be4f240efa3..ad6f32c5611 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -43,7 +43,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Flick Electric: test-username" assert result2["data"] == CONF assert len(mock_setup_entry.mock_calls) == 1 @@ -65,7 +65,7 @@ async def test_form_duplicate_login(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -77,7 +77,7 @@ async def test_form_invalid_auth(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -89,7 +89,7 @@ async def test_form_cannot_connect(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -101,5 +101,5 @@ async def test_form_generic_exception(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/flipr/test_config_flow.py b/tests/components/flipr/test_config_flow.py index 00c8d7e2401..484cba2f4f4 100644 --- a/tests/components/flipr/test_config_flow.py +++ b/tests/components/flipr/test_config_flow.py @@ -25,7 +25,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER @@ -67,7 +67,7 @@ async def test_nominal_case(hass, mock_setup): assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "flipid" assert result["data"] == { CONF_EMAIL: "dummylogin", @@ -91,7 +91,7 @@ async def test_multiple_flip_id(hass, mock_setup): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "flipr_id" result = await hass.config_entries.flow.async_configure( @@ -101,7 +101,7 @@ async def test_multiple_flip_id(hass, mock_setup): assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "FLIP2" assert result["data"] == { CONF_EMAIL: "dummylogin", diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py index 8c22d5fc915..c0fe58ea811 100644 --- a/tests/components/flunearyou/test_config_flow.py +++ b/tests/components/flunearyou/test_config_flow.py @@ -14,7 +14,7 @@ async def test_duplicate_error(hass, config, config_entry, setup_flunearyou): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -35,7 +35,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -44,7 +44,7 @@ async def test_step_user(hass, config, setup_flunearyou): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "51.528308, -0.3817765" assert result["data"] == { CONF_LATITUDE: 51.528308, diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index fd4d82b177c..e810b0eb957 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -58,7 +58,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -78,7 +78,7 @@ async def test_config_flow(hass, config_entry): DOMAIN, context={"source": SOURCE_USER}, data=config_data ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "My Music on myhost" assert result["data"][CONF_HOST] == config_data[CONF_HOST] assert result["data"][CONF_PORT] == config_data[CONF_PORT] @@ -91,7 +91,7 @@ async def test_config_flow(hass, config_entry): data=config_entry.data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_zeroconf_updates_title(hass, config_entry): @@ -112,7 +112,7 @@ async def test_zeroconf_updates_title(hass, config_entry): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert config_entry.title == "zeroconf_test" assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -128,7 +128,7 @@ async def test_config_flow_no_websocket(hass, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config_entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_config_flow_zeroconf_invalid(hass): @@ -146,7 +146,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with forked-daapd version < 27 discovery_info = zeroconf.ZeroconfServiceInfo( @@ -161,7 +161,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with verbose mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -176,7 +176,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with svn mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -191,7 +191,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" @@ -213,7 +213,7 @@ async def test_config_flow_zeroconf_valid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_options_flow(hass, config_entry): @@ -229,7 +229,7 @@ async def test_options_flow(hass, config_entry): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -240,4 +240,4 @@ async def test_options_flow(hass, config_entry): CONF_MAX_PLAYLISTS: 8, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 63c30c16bab..e8bdb6900f2 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -80,7 +80,7 @@ async def test_user_valid(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -98,7 +98,7 @@ async def test_user_valid(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CAMERA_NAME assert result["data"] == VALID_CONFIG @@ -111,7 +111,7 @@ async def test_user_invalid_auth(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -129,7 +129,7 @@ async def test_user_invalid_auth(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -139,7 +139,7 @@ async def test_user_cannot_connect(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -157,7 +157,7 @@ async def test_user_cannot_connect(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -167,7 +167,7 @@ async def test_user_invalid_response(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -187,7 +187,7 @@ async def test_user_invalid_response(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_response"} @@ -203,7 +203,7 @@ async def test_user_already_configured(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -218,7 +218,7 @@ async def test_user_already_configured(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -228,7 +228,7 @@ async def test_user_unknown_exception(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -243,5 +243,5 @@ async def test_user_unknown_exception(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index cee1c28cebd..7922655b3b6 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -53,7 +53,7 @@ async def test_user(hass: HomeAssistant): context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -64,7 +64,7 @@ async def test_import(hass: HomeAssistant): context={"source": SOURCE_IMPORT}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -75,7 +75,7 @@ async def test_zeroconf(hass: HomeAssistant): context={"source": SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -94,7 +94,7 @@ async def test_link(hass: HomeAssistant, router: Mock): ) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == MOCK_HOST assert result["title"] == MOCK_HOST assert result["data"][CONF_HOST] == MOCK_HOST @@ -118,7 +118,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): context={"source": SOURCE_IMPORT}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same MOCK_HOST (flow) @@ -127,7 +127,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -144,7 +144,7 @@ async def test_on_link_failed(hass: HomeAssistant): side_effect=AuthorizationError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "register_failed"} with patch( @@ -152,7 +152,7 @@ async def test_on_link_failed(hass: HomeAssistant): side_effect=HttpRequestError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -160,5 +160,5 @@ async def test_on_link_failed(hass: HomeAssistant): side_effect=InvalidTokenError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py index 42dc0674d07..d1804437a59 100644 --- a/tests/components/freedompro/test_config_flow.py +++ b/tests/components/freedompro/test_config_flow.py @@ -19,7 +19,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -77,6 +77,6 @@ async def test_create_entry(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Freedompro" assert result["data"][CONF_API_KEY] == "ksdjfgslkjdfksjdfksjgfksjd" diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index 8496f0ca5a2..a928f5f5810 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -29,7 +29,7 @@ async def test_duplicate_error(hass, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -38,7 +38,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_step_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -75,7 +75,7 @@ async def test_step_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 592d139f92e..d303e064c1f 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -68,7 +68,7 @@ async def test_form(hass, fakeimg_png, user_flow, mock_create_stream): user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -104,7 +104,7 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -131,7 +131,7 @@ async def test_form_only_stillimage_gif(hass, fakeimg_gif, user_flow): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" @@ -148,7 +148,7 @@ async def test_form_only_svg_whitespace(hass, fakeimgbytes_svg, user_flow): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @respx.mock @@ -175,7 +175,7 @@ async def test_form_only_still_sample(hass, user_flow, image_file): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @respx.mock @@ -186,31 +186,31 @@ async def test_form_only_still_sample(hass, user_flow, image_file): ( "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", "http://localhost:8123/static/icons/favicon-apple-180x180.png", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.FlowResultType.CREATE_ENTRY, None, ), ( "{% if 1 %}https://bla{% else %}https://yo{% endif %}", "https://bla/", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.FlowResultType.CREATE_ENTRY, None, ), ( "http://{{example.org", "http://example.org", - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, {"still_image_url": "template_error"}, ), ( "invalid1://invalid:4\\1", "invalid1://invalid:4%5c1", - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, {"still_image_url": "malformed_url"}, ), ( "relative/urls/are/not/allowed.jpg", "relative/urls/are/not/allowed.jpg", - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, {"still_image_url": "relative_url"}, ), ], @@ -246,7 +246,7 @@ async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream): user_flow["flow_id"], data ) assert "errors" not in result2, f"errors={result2['errors']}" - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -280,7 +280,7 @@ async def test_form_only_stream(hass, fakeimgbytes_jpg, mock_create_stream): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "127_0_0_1" assert result3["options"] == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, @@ -314,7 +314,7 @@ async def test_form_still_and_stream_not_provided(hass, user_flow): CONF_VERIFY_SSL: False, }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "no_still_image_or_stream_url"} @@ -492,7 +492,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # try updating the still image url @@ -503,10 +503,10 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result["flow_id"], user_input=data, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY result3 = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "init" # verify that an invalid template reports the correct UI error. @@ -515,7 +515,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result3["flow_id"], user_input=data, ) - assert result4.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result4.get("type") == data_entry_flow.FlowResultType.FORM assert result4["errors"] == {"still_image_url": "template_error"} # verify that an invalid template reports the correct UI error. @@ -526,7 +526,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream user_input=data, ) - assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5.get("type") == data_entry_flow.FlowResultType.FORM assert result5["errors"] == {"stream_source": "template_error"} # verify that an relative stream url is rejected. @@ -536,7 +536,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result5["flow_id"], user_input=data, ) - assert result6.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result6.get("type") == data_entry_flow.FlowResultType.FORM assert result6["errors"] == {"stream_source": "relative_url"} # verify that an malformed stream url is rejected. @@ -546,7 +546,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result6["flow_id"], user_input=data, ) - assert result7.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result7.get("type") == data_entry_flow.FlowResultType.FORM assert result7["errors"] == {"stream_source": "malformed_url"} @@ -583,7 +583,7 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # try updating the config options @@ -592,7 +592,7 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): result["flow_id"], user_input=data, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" @@ -607,12 +607,12 @@ async def test_import(hass, fakeimg_png): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Yaml Defined Name" await hass.async_block_till_done() # Any name defined in yaml should end up as the entity id. assert hass.states.get("camera.yaml_defined_name") - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT # These above can be deleted after deprecation period is finished. @@ -707,7 +707,7 @@ async def test_use_wallclock_as_timestamps_option( result = await hass.config_entries.options.async_init( mock_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" with patch( "homeassistant.components.generic.async_setup_entry", return_value=True @@ -716,4 +716,4 @@ async def test_use_wallclock_as_timestamps_option( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 013450212ed..9959023e8a6 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -157,10 +157,10 @@ async def webhook_id(hass, geofency_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index 9b471051656..4e00f691884 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -25,7 +25,7 @@ async def test_duplicate_error(hass, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -34,7 +34,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -56,7 +56,7 @@ async def test_step_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -81,7 +81,7 @@ async def test_step_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py index 92c25e00927..a4e1f0587b8 100644 --- a/tests/components/geonetnz_volcano/test_config_flow.py +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -32,7 +32,7 @@ async def test_show_form(hass): result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_step_import(hass): "homeassistant.components.geonetnz_volcano.async_setup", return_value=True ): result = await flow.async_step_import(import_config=conf) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -81,7 +81,7 @@ async def test_step_user(hass): "homeassistant.components.geonetnz_volcano.async_setup", return_value=True ): result = await flow.async_step_user(user_input=conf) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 21fc9d8bfda..8ebb514a4d3 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -25,7 +25,7 @@ async def test_show_form(hass): result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -98,7 +98,7 @@ async def test_create_entry(hass): result = await flow.async_step_user(user_input=CONFIG) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Test Name 1" assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 1b2c2434fab..d996c3af533 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.glances.Glances.get_data", autospec=True): @@ -109,14 +109,14 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={glances.CONF_SCAN_INTERVAL: 10} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { glances.CONF_SCAN_INTERVAL: 10, } diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 669cace729b..5ebc894fab7 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -34,7 +34,7 @@ async def test_flow_user(hass: HomeAssistant): result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -47,7 +47,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -58,7 +58,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -70,7 +70,7 @@ async def test_flow_user_invalid_host(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_host" @@ -82,7 +82,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -97,14 +97,14 @@ async def test_dhcp_discovery(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MANUFACTURER assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -114,7 +114,7 @@ async def test_dhcp_discovery(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -128,7 +128,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -138,7 +138,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_host" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -148,5 +148,5 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 1426c749552..7f6371c1446 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -36,7 +36,7 @@ async def test_minimum_fields(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -44,7 +44,7 @@ async def test_minimum_fields(hass): MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -60,14 +60,14 @@ async def test_invalid_config_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -91,7 +91,7 @@ async def test_options_flow(hass, mock_config): mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -108,7 +108,7 @@ async def test_options_flow(hass, mock_config): CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -144,7 +144,7 @@ async def test_options_flow_departure_time(hass, mock_config): mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -161,7 +161,7 @@ async def test_options_flow_departure_time(hass, mock_config): CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -192,7 +192,7 @@ async def test_dupe(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -204,13 +204,13 @@ async def test_dupe(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -223,4 +223,4 @@ async def test_dupe(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index a885699ca05..4d246cff589 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -65,10 +65,10 @@ async def webhook_id(hass, gpslogger_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py index 27d290e3b90..81a379e8fd8 100644 --- a/tests/components/gree/test_config_flow.py +++ b/tests/components/gree/test_config_flow.py @@ -23,10 +23,10 @@ async def test_creating_entry_sets_up_climate(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -52,10 +52,10 @@ async def test_creating_entry_has_no_devices(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index ba52e09296c..f4e499a3eda 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -50,7 +50,7 @@ async def test_show_authenticate_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -68,7 +68,7 @@ async def test_incorrect_login(hass): result["flow_id"], FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -110,7 +110,7 @@ async def test_multiple_plant_ids(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "plant" user_input = {CONF_PLANT_ID: "123456"} @@ -119,7 +119,7 @@ async def test_multiple_plant_ids(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" @@ -144,7 +144,7 @@ async def test_one_plant_on_account(hass): result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index a56aa6355e8..5e14fc07223 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -21,7 +21,7 @@ async def test_duplicate_error(hass, config, config_entry, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -34,7 +34,7 @@ async def test_connect_error(hass, config): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -55,13 +55,13 @@ async def test_step_user(hass, config, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -85,13 +85,13 @@ async def test_step_zeroconf(hass, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -115,7 +115,7 @@ async def test_step_zeroconf_already_in_progress(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -136,13 +136,13 @@ async def test_step_dhcp(hass, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -162,7 +162,7 @@ async def test_step_dhcp_already_in_progress(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -188,7 +188,7 @@ async def test_step_dhcp_already_setup_match_mac(hass): macaddress="aa:bb:cc:dd:ab:cd", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -210,5 +210,5 @@ async def test_step_dhcp_already_setup_match_ip(hass): macaddress="aa:bb:cc:dd:ab:cd", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 93f909d3bd4..5df675a0f05 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -20,7 +20,7 @@ async def test_flow_works(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == EMAIL @@ -38,7 +38,7 @@ async def test_flow_works_with_authcode(hass, aioclient_mock): "authorization_code": "c29tZXJhbmRvbXN0cmluZw==", } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == EMAIL @@ -57,12 +57,12 @@ async def test_flow_works_with_2fa(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2fa" with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_2fa({"2fa": 123456}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == EMAIL @@ -81,7 +81,7 @@ async def test_flow_with_unknown_2fa(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_2fa_method" @@ -100,7 +100,7 @@ async def test_flow_invalid_login(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_login" @@ -119,7 +119,7 @@ async def test_flow_invalid_2fa(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2fa" with patch( @@ -128,5 +128,5 @@ async def test_flow_invalid_2fa(hass, aioclient_mock): ): result = await flow.async_step_2fa({"2fa": 123456}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_2fa" diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 7725e9752f5..252cfe8923c 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -201,7 +201,7 @@ async def test_options_flow(hass, mock_hc, mock_write_config): assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -209,7 +209,7 @@ async def test_options_flow(hass, mock_hc, mock_write_config): user_input={"activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4, diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index d1d940671b8..e7a37ab7105 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -18,7 +18,7 @@ async def test_flow_aborts_already_setup(hass, config_entry): flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -27,7 +27,7 @@ async def test_no_host_shows_form(hass): flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -38,7 +38,7 @@ async def test_cannot_connect_shows_error_form(hass, controller): result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "127.0.0.1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_HOST] == "cannot_connect" assert controller.connect.call_count == 1 @@ -54,7 +54,7 @@ async def test_create_entry_when_host_valid(hass, controller): result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == data @@ -70,7 +70,7 @@ async def test_create_entry_when_friendly_name_valid(hass, controller): result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == {CONF_HOST: "127.0.0.1"} @@ -118,7 +118,7 @@ async def test_discovery_flow_aborts_already_setup( flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_ssdp(discovery_data) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -151,5 +151,5 @@ async def test_import_sets_the_unique_id(hass, controller): data={CONF_HOST: "127.0.0.2"}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index c1ce6f823ae..dc9eedfa3f6 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -141,7 +141,7 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -166,7 +166,7 @@ async def test_step_origin_coordinates( menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -189,7 +189,7 @@ async def test_step_origin_entity( menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_entity"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -206,7 +206,7 @@ async def test_step_destination_coordinates( menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_coordinates"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -218,7 +218,9 @@ async def test_step_destination_coordinates( } }, ) - assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + location_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + ) entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -239,13 +241,13 @@ async def test_step_destination_entity( menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_entity"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], {"destination_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entity_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -327,7 +329,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -350,7 +352,7 @@ async def test_options_flow_arrival_time_step( menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "arrival_time"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -358,7 +360,7 @@ async def test_options_flow_arrival_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, @@ -376,7 +378,7 @@ async def test_options_flow_departure_time_step( menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "departure_time"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -384,7 +386,7 @@ async def test_options_flow_departure_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, @@ -403,7 +405,7 @@ async def test_options_flow_no_time_step( option_init_result["flow_id"], {"next_step_id": "no_time"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert menu_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, @@ -434,7 +436,7 @@ async def test_import_flow_entity_id(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "namespace test_name" entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -476,7 +478,7 @@ async def test_import_flow_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test_name" entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -519,7 +521,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -539,7 +541,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -559,7 +561,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -579,7 +581,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -599,5 +601,5 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index ef99c3d1c96..4905638a594 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -22,10 +22,10 @@ async def test_creating_entry_sets_up_climate_discovery(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index e6e2a06501a..21cae9f8366 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -46,7 +46,7 @@ async def test_import_flow(hass): data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == USERNAME assert result["data"] == { CONF_USERNAME: USERNAME, @@ -70,7 +70,7 @@ async def test_user_flow(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -94,7 +94,7 @@ async def test_user_flow(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == USERNAME assert result2["data"] == { CONF_USERNAME: USERNAME, @@ -119,7 +119,7 @@ async def test_user_flow_2fa(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -136,7 +136,7 @@ async def test_user_flow_2fa(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -157,7 +157,7 @@ async def test_user_flow_2fa(hass): }, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -185,7 +185,7 @@ async def test_user_flow_2fa(hass): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == USERNAME assert result4["data"] == { CONF_USERNAME: USERNAME, @@ -239,7 +239,7 @@ async def test_reauth_flow(hass): data=mock_config.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -263,7 +263,7 @@ async def test_reauth_flow(hass): assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -298,7 +298,7 @@ async def test_reauth_2fa_flow(hass): data=mock_config.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -338,7 +338,7 @@ async def test_reauth_2fa_flow(hass): assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -370,14 +370,14 @@ async def test_option_flow(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: UPDATED_SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == UPDATED_SCAN_INTERVAL @@ -387,7 +387,7 @@ async def test_user_flow_2fa_send_new_code(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -404,7 +404,7 @@ async def test_user_flow_2fa_send_new_code(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -419,7 +419,7 @@ async def test_user_flow_2fa_send_new_code(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {} @@ -440,7 +440,7 @@ async def test_user_flow_2fa_send_new_code(hass): }, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {} @@ -465,7 +465,7 @@ async def test_user_flow_2fa_send_new_code(hass): ) await hass.async_block_till_done() - assert result5["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result5["title"] == USERNAME assert result5["data"] == { CONF_USERNAME: USERNAME, @@ -507,7 +507,7 @@ async def test_abort_if_existing_entry(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -517,7 +517,7 @@ async def test_user_flow_invalid_username(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -529,7 +529,7 @@ async def test_user_flow_invalid_username(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_username"} @@ -540,7 +540,7 @@ async def test_user_flow_invalid_password(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -552,7 +552,7 @@ async def test_user_flow_invalid_password(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_password"} @@ -564,7 +564,7 @@ async def test_user_flow_no_internet_connection(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -576,7 +576,7 @@ async def test_user_flow_no_internet_connection(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "no_internet_available"} @@ -588,7 +588,7 @@ async def test_user_flow_2fa_no_internet_connection(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -602,7 +602,7 @@ async def test_user_flow_2fa_no_internet_connection(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -615,7 +615,7 @@ async def test_user_flow_2fa_no_internet_connection(hass): {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "no_internet_available"} @@ -626,7 +626,7 @@ async def test_user_flow_2fa_invalid_code(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -640,7 +640,7 @@ async def test_user_flow_2fa_invalid_code(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -652,7 +652,7 @@ async def test_user_flow_2fa_invalid_code(hass): result["flow_id"], {CONF_CODE: MFA_INVALID_CODE}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "invalid_code"} @@ -663,7 +663,7 @@ async def test_user_flow_unknown_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -676,7 +676,7 @@ async def test_user_flow_unknown_error(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -686,7 +686,7 @@ async def test_user_flow_2fa_unknown_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -700,7 +700,7 @@ async def test_user_flow_2fa_unknown_error(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE with patch( @@ -712,7 +712,7 @@ async def test_user_flow_2fa_unknown_error(hass): {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -733,6 +733,6 @@ async def test_user_flow_2fa_unknown_error(hass): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {"base": "unknown"} diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index fa7d3fee8f0..bb1eb8d1897 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -42,7 +42,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" diff --git a/tests/components/home_plus_control/test_config_flow.py b/tests/components/home_plus_control/test_config_flow.py index cdf1f85f187..e86362c3e11 100644 --- a/tests/components/home_plus_control/test_config_flow.py +++ b/tests/components/home_plus_control/test_config_flow.py @@ -47,7 +47,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "auth" assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" @@ -77,7 +77,7 @@ async def test_full_flow( result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Home+ Control" config_data = result["data"] assert config_data["token"]["refresh_token"] == "mock-refresh-token" @@ -109,7 +109,7 @@ async def test_abort_if_entry_in_progress(hass, current_request_with_host): result = await hass.config_entries.flow.async_init( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -134,7 +134,7 @@ async def test_abort_if_entry_exists(hass, current_request_with_host): result = await hass.config_entries.flow.async_init( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -165,7 +165,7 @@ async def test_abort_if_invalid_token( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "auth" assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" @@ -189,5 +189,5 @@ async def test_abort_if_invalid_token( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "oauth_error" diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index ce0bed0ff52..1b2a0b4e211 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -56,7 +56,7 @@ async def test_setup_in_bridge_mode(hass, mock_get_source_ip): result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" with patch( @@ -74,7 +74,7 @@ async def test_setup_in_bridge_mode(hass, mock_get_source_ip): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY bridge_name = (result3["title"].split(":"))[0] assert bridge_name == SHORT_BRIDGE_NAME assert result3["data"] == { @@ -112,7 +112,7 @@ async def test_setup_in_bridge_mode_name_taken(hass, mock_get_source_ip): result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" with patch( @@ -130,7 +130,7 @@ async def test_setup_in_bridge_mode_name_taken(hass, mock_get_source_ip): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] != SHORT_BRIDGE_NAME assert result3["title"].startswith(SHORT_BRIDGE_NAME) bridge_name = (result3["title"].split(":"))[0] @@ -194,7 +194,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( result["flow_id"], {"include_domains": ["camera", "media_player", "light", "lock", "remote"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" with patch( @@ -212,7 +212,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -257,7 +257,7 @@ async def test_import(hass, mock_get_source_ip): context={"source": config_entries.SOURCE_IMPORT}, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "port_name_in_use" with patch( @@ -273,7 +273,7 @@ async def test_import(hass, mock_get_source_ip): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "othername:56789" assert result2["data"] == { "name": "othername", @@ -296,7 +296,7 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -307,14 +307,14 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "advanced" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -323,7 +323,7 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): user_input={}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [], "mode": "bridge", @@ -351,7 +351,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -362,7 +362,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] assert entities == ["climate.front_gate"] @@ -375,7 +375,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -430,7 +430,7 @@ async def test_options_flow_devices( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -441,7 +441,7 @@ async def test_options_flow_devices( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" entry = entity_reg.async_get("light.ceiling_lights") @@ -461,7 +461,7 @@ async def test_options_flow_devices( user_input={"devices": [device_id]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [device_id], "mode": "bridge", @@ -510,7 +510,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -521,7 +521,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -531,7 +531,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": ["1fabcabcabcabcabcabcabcabcabc"], "mode": "bridge", @@ -567,7 +567,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -578,7 +578,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" entities = result["data_schema"]({})["entities"] @@ -590,7 +590,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -626,7 +626,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -637,7 +637,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] @@ -649,7 +649,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -676,7 +676,7 @@ async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -687,14 +687,14 @@ async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.new"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -723,7 +723,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -734,7 +734,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -743,7 +743,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -751,7 +751,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -769,7 +769,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -780,7 +780,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -789,7 +789,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -797,7 +797,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", @@ -828,7 +828,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -839,7 +839,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -848,7 +848,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): "entities": ["camera.native_h264", "camera.transcode_h264"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -856,7 +856,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -874,7 +874,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["fan", "vacuum", "climate", "camera"], @@ -899,7 +899,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.native_h264", "camera.transcode_h264"], @@ -916,7 +916,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": ["camera.native_h264"], @@ -930,7 +930,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.native_h264": {}}, "filter": { @@ -960,7 +960,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -971,7 +971,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -980,7 +980,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): "entities": ["camera.audio", "camera.no_audio"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -988,7 +988,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): user_input={"camera_audio": ["camera.audio"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1006,7 +1006,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["fan", "vacuum", "climate", "camera"], @@ -1031,7 +1031,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.audio", "camera.no_audio"], @@ -1048,7 +1048,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": [], @@ -1062,7 +1062,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): user_input={"camera_audio": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.audio": {}}, "filter": { @@ -1103,7 +1103,7 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "yaml" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -1111,7 +1111,7 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) @@ -1131,7 +1131,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1151,7 +1151,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "accessory" assert _get_schema_default(result2["data_schema"].schema, "entities") is None @@ -1159,7 +1159,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1177,7 +1177,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["media_player"], @@ -1190,7 +1190,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "accessory" assert ( _get_schema_default(result2["data_schema"].schema, "entities") @@ -1201,7 +1201,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1226,7 +1226,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" # We need to actually setup the config entry or the data @@ -1244,7 +1244,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -1272,7 +1272,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert _get_schema_default(schema, "mode") == "bridge" @@ -1283,14 +1283,14 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou user_input={"domains": ["camera"], "mode": "accessory"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "accessory" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": "camera.tv"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" with patch( @@ -1305,7 +1305,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.tv": {"video_codec": "copy"}}, "mode": "accessory", @@ -1363,7 +1363,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1387,7 +1387,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1409,7 +1409,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( ] }, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1451,7 +1451,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1475,7 +1475,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1491,7 +1491,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( result2["flow_id"], user_input={"entities": ["media_player.tv", "switch.other"]}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1529,7 +1529,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1553,7 +1553,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "include" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1569,7 +1569,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( ] }, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index b93e359c1ac..d877133bdcd 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -53,7 +53,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG @@ -70,7 +70,7 @@ async def test_show_option_form( result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" @@ -91,7 +91,7 @@ async def test_create_option_entry( user_input={CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2, diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index b363836cc4f..84f66e8f0ab 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -41,7 +41,7 @@ async def test_show_set_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -54,7 +54,7 @@ async def test_urlize_plain_host(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert user_input[CONF_URL] == f"http://{host}/" @@ -85,7 +85,7 @@ async def test_already_configured(hass, requests_mock, login_requests_mock): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -96,7 +96,7 @@ async def test_connection_error(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "unknown"} @@ -142,7 +142,7 @@ async def test_login_error(hass, login_requests_mock, code, errors): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -164,7 +164,7 @@ async def test_success(hass, login_requests_mock): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -195,7 +195,7 @@ async def test_ssdp(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({})[CONF_URL] == url @@ -209,7 +209,7 @@ async def test_options(hass): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" recipient = "+15555550000" diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 4b0ae908a2d..8aac11baf6d 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -68,7 +68,7 @@ async def test_form_invalid_auth(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["type"] == data_entry_flow.FlowResultType.FORM assert form_result["errors"] == {"base": "invalid_auth"} @@ -90,7 +90,7 @@ async def test_form_cannot_connect(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["type"] == data_entry_flow.FlowResultType.FORM assert form_result["errors"] == {"base": "cannot_connect"} @@ -112,7 +112,7 @@ async def test_form_unknown_error(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["type"] == data_entry_flow.FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} @@ -148,5 +148,5 @@ async def test_form_entry_exists(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert form_result["type"] == data_entry_flow.FlowResultType.ABORT assert form_result["reason"] == "already_configured" diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 9c510bb3db0..126e61cf629 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -273,7 +273,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -281,7 +281,7 @@ async def test_options_flow(hass): user_input={CONF_FILTER: ["0"], CONF_OFFSET: 15, CONF_REAL_TIME: False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_FILTER: [ { @@ -329,7 +329,7 @@ async def test_options_flow_invalid_auth(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_auth"} @@ -364,7 +364,7 @@ async def test_options_flow_cannot_connect(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 5d2e77e8adb..27f6f25856d 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -166,7 +166,7 @@ async def test_user_if_no_configuration(hass: HomeAssistant) -> None: """Check flow behavior when no configuration is present.""" result = await _init_flow(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["handler"] == DOMAIN @@ -181,7 +181,7 @@ async def test_user_existing_id_abort(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -197,7 +197,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Fail the auth check call. @@ -207,7 +207,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -226,7 +226,7 @@ async def test_user_confirm_cannot_connect(hass: HomeAssistant) -> None: side_effect=[good_client, bad_client], ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -242,7 +242,7 @@ async def test_user_confirm_id_error(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_id" @@ -256,7 +256,7 @@ async def test_user_noauth_flow_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -275,7 +275,7 @@ async def test_user_auth_required(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -289,7 +289,7 @@ async def test_auth_static_token_auth_required_fail(hass: HomeAssistant) -> None "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -309,7 +309,7 @@ async def test_auth_static_token_success(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -335,7 +335,7 @@ async def test_auth_static_token_login_connect_fail(hass: HomeAssistant) -> None hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -358,7 +358,7 @@ async def test_auth_static_token_login_fail(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_access_token" @@ -373,7 +373,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -387,7 +387,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, @@ -395,13 +395,13 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_granted_error" @@ -479,7 +479,7 @@ async def test_auth_create_token_when_issued_token_fails( "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -492,14 +492,14 @@ async def test_auth_create_token_when_issued_token_fails( result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. @@ -508,7 +508,7 @@ async def test_auth_create_token_when_issued_token_fails( client.async_client_connect = AsyncMock(return_value=False) result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -523,7 +523,7 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -536,19 +536,19 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -594,7 +594,7 @@ async def test_auth_create_token_success_but_login_fail( # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_work_error" @@ -614,7 +614,7 @@ async def test_ssdp_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -635,7 +635,7 @@ async def test_ssdp_cannot_connect(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -653,7 +653,7 @@ async def test_ssdp_missing_serial(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_id" @@ -672,7 +672,7 @@ async def test_ssdp_failure_bad_port_json(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_PORT] == const.DEFAULT_PORT_JSON @@ -693,7 +693,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: ): result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -702,7 +702,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" # Verify a working URL is used despite the bad port number @@ -726,8 +726,8 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result_1["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result_2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_1["type"] == data_entry_flow.FlowResultType.FORM + assert result_2["type"] == data_entry_flow.FlowResultType.ABORT assert result_2["reason"] == "already_in_progress" @@ -745,7 +745,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: assert hass.states.get(TEST_ENTITY_ID_1) is not None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" new_priority = 1 @@ -754,7 +754,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: user_input={CONF_PRIORITY: new_priority}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_PRIORITY] == new_priority # Turn the light on and ensure the new priority is used. @@ -788,14 +788,14 @@ async def test_options_effect_show_list(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_EFFECT_SHOW_LIST: ["effect1", "effect3"]}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # effect1 and effect3 only, so effect2 & external sources are hidden. assert result["data"][CONF_EFFECT_HIDE_LIST] == sorted( @@ -818,7 +818,7 @@ async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistant) -> N client.async_client_connect = AsyncMock(return_value=False) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -845,13 +845,13 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert CONF_TOKEN in config_entry.data @@ -877,5 +877,5 @@ async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/ialarm/test_config_flow.py b/tests/components/ialarm/test_config_flow.py index 102b2edb0a6..f7c0a4ed338 100644 --- a/tests/components/ialarm/test_config_flow.py +++ b/tests/components/ialarm/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DATA["host"] assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -56,7 +56,7 @@ async def test_form_cannot_connect(hass): result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -74,7 +74,7 @@ async def test_form_exception(hass): result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -100,5 +100,5 @@ async def test_form_already_exists(hass): result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index cd72aae0eff..3c854d468b8 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -162,7 +162,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with required @@ -171,7 +171,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -187,7 +187,7 @@ async def test_user_with_cookie(hass: HomeAssistant, service_authenticated: Magi CONF_WITH_FAMILY: WITH_FAMILY, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -208,7 +208,7 @@ async def test_login_failed(hass: HomeAssistant): context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -221,7 +221,7 @@ async def test_no_device( context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_device" @@ -234,7 +234,7 @@ async def test_trusted_device(hass: HomeAssistant, service: MagicMock): ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -249,7 +249,7 @@ async def test_trusted_device_success(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -266,7 +266,7 @@ async def test_send_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {CONF_TRUSTED_DEVICE: "send_verification_code"} @@ -283,7 +283,7 @@ async def test_verification_code(hass: HomeAssistant, service: MagicMock): ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -302,7 +302,7 @@ async def test_verification_code_success(hass: HomeAssistant, service: MagicMock result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -328,7 +328,7 @@ async def test_validate_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {"base": "validate_verification_code"} @@ -347,7 +347,7 @@ async def test_2fa_code_success(hass: HomeAssistant, service_2fa: MagicMock): result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -371,7 +371,7 @@ async def test_validate_2fa_code_failed( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE assert result["errors"] == {"base": "validate_verification_code"} @@ -389,13 +389,13 @@ async def test_password_update(hass: HomeAssistant, service_authenticated: Magic data={**MOCK_CONFIG, "unique_id": USERNAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_PASSWORD] == PASSWORD_2 @@ -413,7 +413,7 @@ async def test_password_update_wrong_password(hass: HomeAssistant): data={**MOCK_CONFIG, "unique_id": USERNAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.icloud.config_flow.PyiCloudService.authenticate", @@ -423,5 +423,5 @@ async def test_password_update_wrong_password(hass: HomeAssistant): result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index 1ca8395e11f..59bf51de9bd 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -15,10 +15,10 @@ async def test_config_flow_registers_webhook(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "ifttt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] ifttt_events = [] diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 878b540b721..35a32ef969c 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -71,7 +71,7 @@ async def _init_form(hass, modem_type): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -118,7 +118,7 @@ async def test_fail_on_existing(hass: HomeAssistant): data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -223,7 +223,7 @@ async def _options_init_form(hass, entry_id, step): with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): result = await hass.config_entries.options.async_init(entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -279,7 +279,7 @@ async def test_import_existing(hass: HomeAssistant): result = await _import_config( hass, {**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -295,7 +295,7 @@ async def test_import_failed_connection(hass: HomeAssistant): data={**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -329,7 +329,7 @@ async def test_options_change_hub_config(hass: HomeAssistant): } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {} assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} @@ -353,7 +353,7 @@ async def test_options_add_device_override(hass: HomeAssistant): } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 assert config_entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" assert config_entry.options[CONF_OVERRIDE][0][CONF_CAT] == 4 @@ -397,7 +397,7 @@ async def test_options_remove_device_override(hass: HomeAssistant): user_input = {CONF_ADDRESS: "1A.2B.3C"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 @@ -429,7 +429,7 @@ async def test_options_remove_device_override_with_x10(hass: HomeAssistant): user_input = {CONF_ADDRESS: "1A.2B.3C"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 assert len(config_entry.options[CONF_X10]) == 1 @@ -454,7 +454,7 @@ async def test_options_add_x10_device(hass: HomeAssistant): } result2, _ = await _options_form(hass, result["flow_id"], user_input) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 assert config_entry.options[CONF_X10][0][CONF_HOUSECODE] == "c" assert config_entry.options[CONF_X10][0][CONF_UNITCODE] == 12 @@ -470,7 +470,7 @@ async def test_options_add_x10_device(hass: HomeAssistant): } result3, _ = await _options_form(hass, result["flow_id"], user_input) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 2 assert config_entry.options[CONF_X10][1][CONF_HOUSECODE] == "d" assert config_entry.options[CONF_X10][1][CONF_UNITCODE] == 10 @@ -511,7 +511,7 @@ async def test_options_remove_x10_device(hass: HomeAssistant): user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 @@ -546,7 +546,7 @@ async def test_options_remove_x10_device_with_override(hass: HomeAssistant): user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 assert len(config_entry.options[CONF_OVERRIDE]) == 1 @@ -562,14 +562,14 @@ async def test_options_dup_selection(hass: HomeAssistant): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( result["flow_id"], {STEP_ADD_OVERRIDE: True, STEP_ADD_X10: True}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "select_single"} @@ -593,7 +593,7 @@ async def test_options_override_bad_data(hass: HomeAssistant): } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "input_error"} @@ -611,7 +611,7 @@ async def test_discovery_via_usb(hass): "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm_usb" with patch("homeassistant.components.insteon.config_flow.async_connect"), patch( @@ -622,7 +622,7 @@ async def test_discovery_via_usb(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == {"device": "/dev/ttyINSTEON"} @@ -646,5 +646,5 @@ async def test_discovery_via_usb_already_setup(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 315098a44d8..0d881c30a35 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -9,7 +9,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -18,7 +18,7 @@ async def test_invalid_zip_code(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_ZIP_CODE: "bad"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_ZIP_CODE: "invalid_zip_code"} @@ -27,7 +27,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -36,6 +36,6 @@ async def test_step_user(hass, config, setup_iqvia): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == {CONF_ZIP_CODE: "12345"} diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 730c5634770..d447da1c61e 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -26,13 +26,13 @@ async def test_flow_works(hass): result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Islamic Prayer Times" @@ -48,14 +48,14 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_CALC_METHOD: "makkah"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_CALC_METHOD] == "makkah" @@ -71,5 +71,5 @@ async def test_integration_already_configured(hass): islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index 3260b76432f..eabca610ddf 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -18,7 +18,7 @@ async def test_create_entry(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == SOURCE_USER with patch("homeassistant.components.iss.async_setup_entry", return_value=True): @@ -28,7 +28,7 @@ async def test_create_entry(hass: HomeAssistant): {}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -44,7 +44,7 @@ async def test_integration_already_exists(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -60,7 +60,7 @@ async def test_abort_no_home(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "latitude_longitude_not_defined" diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 8458dc0dc67..b87662718e5 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -116,7 +116,7 @@ async def test_form(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( @@ -130,7 +130,7 @@ async def test_form(hass: HomeAssistant): MOCK_USER_INPUT, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -154,7 +154,7 @@ async def test_form_invalid_host(hass: HomeAssistant): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_host"} @@ -172,7 +172,7 @@ async def test_form_invalid_auth(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -190,7 +190,7 @@ async def test_form_unknown_exeption(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -208,7 +208,7 @@ async def test_form_isy_connection_error(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -226,7 +226,7 @@ async def test_form_isy_parse_response_error(hass: HomeAssistant, caplog): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert "ISY Could not parse response, poorly formatted XML." in caplog.text @@ -246,7 +246,7 @@ async def test_form_no_name_in_response(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -257,7 +257,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE): @@ -265,7 +265,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant): result["flow_id"], MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT async def test_import_flow_some_fields(hass: HomeAssistant) -> None: @@ -282,7 +282,7 @@ async def test_import_flow_some_fields(hass: HomeAssistant) -> None: data=MOCK_IMPORT_BASIC_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}" assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD @@ -303,7 +303,7 @@ async def test_import_flow_with_https(hass: HomeAssistant) -> None: data=MOCK_IMPORT_WITH_SSL, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == f"https://{MOCK_HOSTNAME}" assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD @@ -323,7 +323,7 @@ async def test_import_flow_all_fields(hass: HomeAssistant) -> None: data=MOCK_IMPORT_FULL_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}" assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD @@ -356,7 +356,7 @@ async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_form_ssdp(hass: HomeAssistant): @@ -375,7 +375,7 @@ async def test_form_ssdp(hass: HomeAssistant): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -391,7 +391,7 @@ async def test_form_ssdp(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -425,7 +425,7 @@ async def test_form_ssdp_existing_entry(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80{ISY_URL_POSTFIX}" @@ -456,7 +456,7 @@ async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80/{ISY_URL_POSTFIX}" @@ -487,7 +487,7 @@ async def test_form_ssdp_existing_entry_with_alternate_port(hass: HomeAssistant) ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}" @@ -518,7 +518,7 @@ async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"https://3.3.3.3:443/{ISY_URL_POSTFIX}" @@ -535,7 +535,7 @@ async def test_form_dhcp(hass: HomeAssistant): macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -551,7 +551,7 @@ async def test_form_dhcp(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -571,7 +571,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant): macaddress=MOCK_POLISY_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} assert ( @@ -591,7 +591,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_POLISY_USER_INPUT @@ -621,7 +621,7 @@ async def test_form_dhcp_existing_entry(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4{ISY_URL_POSTFIX}" @@ -651,7 +651,7 @@ async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4:1443{ISY_URL_POSTFIX}" assert entry.data[CONF_USERNAME] == "bob" @@ -677,7 +677,7 @@ async def test_form_dhcp_existing_ignored_entry(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index a548e59930b..d48e19181c8 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -42,10 +42,10 @@ async def test_not_found(hass, mock_disco): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() @@ -71,10 +71,10 @@ async def test_found(hass, mock_disco): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index cc23265e011..e898f8ac5ce 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -27,7 +27,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 18d5f3df1fb..e1cb083dc73 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -51,7 +51,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -63,7 +63,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.options.async_configure( @@ -106,7 +106,7 @@ async def test_options(hass: HomeAssistant) -> None: user_input=MOCK_OPTIONS, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == MOCK_OPTIONS @@ -126,7 +126,7 @@ async def test_host_already_configured(hass: HomeAssistant, connect) -> None: result["flow_id"], user_input=MOCK_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -139,7 +139,7 @@ async def test_connection_error(hass: HomeAssistant, connect_error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -153,7 +153,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -168,7 +168,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -189,7 +189,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -210,7 +210,7 @@ async def test_ssdp_ignored(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -236,7 +236,7 @@ async def test_ssdp_update_host(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == new_ip @@ -254,7 +254,7 @@ async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_udn" @@ -270,5 +270,5 @@ async def test_ssdp_reject_non_keenetic(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_keenetic_ndms2" diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index fabd8738259..c2965dfa083 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -69,14 +69,14 @@ async def test_form_options(hass, aioclient_mock): assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_REVERSE: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_REVERSE: True} await hass.async_block_till_done() diff --git a/tests/components/launch_library/test_config_flow.py b/tests/components/launch_library/test_config_flow.py index 5b6cb85cfee..a9dd794d05e 100644 --- a/tests/components/launch_library/test_config_flow.py +++ b/tests/components/launch_library/test_config_flow.py @@ -15,7 +15,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == SOURCE_USER with patch( @@ -27,7 +27,7 @@ async def test_create_entry(hass): {}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -43,5 +43,5 @@ async def test_integration_already_exists(hass): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index 36b5d23739a..6f084f939d8 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -43,7 +43,7 @@ async def test_step_import(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "pchk" assert result["data"] == IMPORT_DATA @@ -65,7 +65,7 @@ async def test_step_import_existing_host(hass): await hass.async_block_till_done() # Check if config entry was updated - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "existing_configuration_updated" assert mock_entry.source == config_entries.SOURCE_IMPORT assert mock_entry.data == IMPORT_DATA @@ -91,5 +91,5 @@ async def test_step_import_error(hass, error, reason): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py index 0b5b850ac23..02d5539e117 100644 --- a/tests/components/life360/test_config_flow.py +++ b/tests/components/life360/test_config_flow.py @@ -106,7 +106,7 @@ async def test_user_show_form(hass, life360_api): life360_api.get_authorization.assert_not_called() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -134,7 +134,7 @@ async def test_user_config_flow_success(hass, life360_api): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USER.lower() assert result["data"] == TEST_CONFIG_DATA assert result["options"] == DEFAULT_OPTIONS @@ -159,7 +159,7 @@ async def test_user_config_flow_error(hass, life360_api, caplog, exception, erro life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] assert result["errors"]["base"] == error @@ -190,7 +190,7 @@ async def test_user_config_flow_already_configured(hass, life360_api): life360_api.get_authorization.assert_not_called() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -220,7 +220,7 @@ async def test_reauth_config_flow_success(hass, life360_api, caplog, state): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert "Reauthorization successful" in caplog.text @@ -250,7 +250,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, caplog): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_auth" @@ -274,7 +274,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, caplog): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert "Reauthorization successful" in caplog.text @@ -292,7 +292,7 @@ async def test_options_flow(hass): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert not result["errors"] @@ -303,7 +303,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_configure(flow_id, USER_OPTIONS) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == TEST_OPTIONS assert config_entry.options == TEST_OPTIONS diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index cfae178f792..18d5dff80db 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -85,7 +85,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -93,5 +93,5 @@ async def test_options(hass): user_input={CONF_DEFAULT_TRANSITION: 12}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEFAULT_TRANSITION: 12} diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index 4804ab83aca..1b84e3e8552 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -11,10 +11,10 @@ async def test_config_flow(hass, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get(f"sensor.{DOMAIN}") @@ -32,5 +32,5 @@ async def test_already_setup(hass, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index f65e9d8a6af..94b94c2ec93 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -41,10 +41,10 @@ async def webhook_id(hass, locative_client): result = await hass.config_entries.flow.async_init( "locative", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 536d370f16a..2f1d69b8d47 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -66,7 +66,7 @@ async def test_step_import( flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -86,18 +86,18 @@ async def test_full_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test-other"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "http://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Logi Circle ({})".format("testId") @@ -115,7 +115,7 @@ async def test_abort_if_no_implementation_registered(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -128,21 +128,21 @@ async def test_abort_if_already_setup(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() result = await flow.async_step_auth() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "external_setup" @@ -161,7 +161,7 @@ async def test_abort_if_authorize_fails( mock_logi_circle.authorize.side_effect = side_effect result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "external_error" result = await flow.async_step_auth() @@ -173,7 +173,7 @@ async def test_not_pick_implementation_if_only_one(hass): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 821bf07cf08..b5e8271d351 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -101,7 +101,7 @@ async def test_bridge_cannot_connect(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -124,7 +124,7 @@ async def test_bridge_cannot_connect_unknown_error(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -144,7 +144,7 @@ async def test_bridge_invalid_ssl_error(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -171,7 +171,7 @@ async def test_duplicate_bridge_import(hass): data=entry_mock_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index d9262e215fb..6eb97f03f89 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -40,7 +40,7 @@ async def test_abort_if_no_configuration(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -71,7 +71,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -132,7 +132,7 @@ async def test_abort_if_authorization_timeout( ): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -196,7 +196,7 @@ async def test_reauthentication_flow( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index c6eeb9f99ad..2dc18b63d5b 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -37,10 +37,10 @@ async def webhook_id_with_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] @@ -57,10 +57,10 @@ async def webhook_id_without_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py index c2628f2aa11..1090d928952 100644 --- a/tests/components/mazda/test_config_flow.py +++ b/tests/components/mazda/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -71,7 +71,7 @@ async def test_account_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -95,7 +95,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -109,7 +109,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -120,7 +120,7 @@ async def test_form_account_locked(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -134,7 +134,7 @@ async def test_form_account_locked(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "account_locked"} @@ -206,7 +206,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -220,7 +220,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -249,7 +249,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -258,7 +258,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -288,7 +288,7 @@ async def test_reauth_account_locked(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -297,7 +297,7 @@ async def test_reauth_account_locked(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "account_locked"} @@ -327,7 +327,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -336,7 +336,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -366,7 +366,7 @@ async def test_reauth_unknown_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -375,7 +375,7 @@ async def test_reauth_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -405,7 +405,7 @@ async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Change the email and ensure the entry and its unique id gets @@ -419,5 +419,5 @@ async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: assert ( mock_config.unique_id == FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL[CONF_EMAIL] ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/meater/test_config_flow.py b/tests/components/meater/test_config_flow.py index 11312111311..0d05466e464 100644 --- a/tests/components/meater/test_config_flow.py +++ b/tests/components/meater/test_config_flow.py @@ -37,7 +37,7 @@ async def test_duplicate_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -83,7 +83,7 @@ async def test_user_flow(hass, mock_meater): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -93,7 +93,7 @@ async def test_user_flow(hass, mock_meater): result = await hass.config_entries.flow.async_configure(result["flow_id"], conf) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123", @@ -126,7 +126,7 @@ async def test_reauth_flow(hass, mock_meater): data=data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] is None @@ -136,7 +136,7 @@ async def test_reauth_flow(hass, mock_meater): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" config_entry = hass.config_entries.async_entries(DOMAIN)[0] diff --git a/tests/components/met_eireann/test_config_flow.py b/tests/components/met_eireann/test_config_flow.py index 50060541be5..334e4a52ac3 100644 --- a/tests/components/met_eireann/test_config_flow.py +++ b/tests/components/met_eireann/test_config_flow.py @@ -64,7 +64,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == test_data.get("name") assert result["data"] == test_data @@ -85,11 +85,11 @@ async def test_flow_entry_already_exists(hass): result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Create the second entry and assert that it is aborted result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 29c08e41d1d..295e8a1e5e8 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -121,7 +121,7 @@ async def test_user(hass, client_single): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided with search returning only 1 place @@ -130,7 +130,7 @@ async def test_user(hass, client_single): context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_1_LAT}, {CITY_1_LON}" assert result["title"] == f"{CITY_1}" assert result["data"][CONF_LATITUDE] == str(CITY_1_LAT) @@ -146,14 +146,14 @@ async def test_user_list(hass, client_multiple): context={"source": SOURCE_USER}, data={CONF_CITY: CITY_2_NAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "cities" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_CITY: f"{CITY_3};{CITY_3_LAT};{CITY_3_LON}"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_3_LAT}, {CITY_3_LON}" assert result["title"] == f"{CITY_3}" assert result["data"][CONF_LATITUDE] == str(CITY_3_LAT) @@ -168,7 +168,7 @@ async def test_import(hass, client_multiple): context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_2_NAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_2_LAT}, {CITY_2_LON}" assert result["title"] == f"{CITY_2}" assert result["data"][CONF_LATITUDE] == str(CITY_2_LAT) @@ -183,7 +183,7 @@ async def test_search_failed(hass, client_empty): data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_CITY: "empty"} @@ -201,7 +201,7 @@ async def test_abort_if_already_setup(hass, client_single): context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same CITY same postal code (flow) @@ -210,7 +210,7 @@ async def test_abort_if_already_setup(hass, client_single): context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -226,7 +226,7 @@ async def test_options_flow(hass: HomeAssistant): assert config_entry.options == {} result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # Default @@ -234,7 +234,7 @@ async def test_options_flow(hass: HomeAssistant): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_MODE] == FORECAST_MODE_DAILY # Manual @@ -243,5 +243,5 @@ async def test_options_flow(hass: HomeAssistant): result["flow_id"], user_input={CONF_MODE: FORECAST_MODE_HOURLY}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_MODE] == FORECAST_MODE_HOURLY diff --git a/tests/components/meteoclimatic/test_config_flow.py b/tests/components/meteoclimatic/test_config_flow.py index e5daaea1978..4b94862553e 100644 --- a/tests/components/meteoclimatic/test_config_flow.py +++ b/tests/components/meteoclimatic/test_config_flow.py @@ -42,7 +42,7 @@ async def test_user(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -51,7 +51,7 @@ async def test_user(hass, client): context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == TEST_STATION_CODE assert result["title"] == TEST_STATION_NAME assert result["data"][CONF_STATION_CODE] == TEST_STATION_CODE @@ -68,7 +68,7 @@ async def test_not_found(hass): context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "not_found" @@ -84,5 +84,5 @@ async def test_unknown_error(hass): context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index b4c087a436d..704bf92066e 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -89,14 +89,14 @@ async def test_flow_works(hass, api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Home router" assert result["data"][CONF_NAME] == "Home router" assert result["data"][CONF_HOST] == "0.0.0.0" @@ -115,7 +115,7 @@ async def test_options(hass, api): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device_tracker" result = await hass.config_entries.options.async_configure( @@ -127,7 +127,7 @@ async def test_options(hass, api): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_DETECTION_TIME: 30, CONF_ARP_PING: True, @@ -179,7 +179,7 @@ async def test_connection_error(hass, conn_error): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -193,7 +193,7 @@ async def test_wrong_credentials(hass, auth_error): result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index 19a98106c63..8dc7eccf25a 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -37,14 +37,14 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: com_port().device} @@ -56,7 +56,7 @@ async def test_flow_usb_cannot_connect(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -78,7 +78,7 @@ async def test_flow_user(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} result = await hass.config_entries.flow.async_init( @@ -86,7 +86,7 @@ async def test_flow_user(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -107,7 +107,7 @@ async def test_flow_user_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -116,7 +116,7 @@ async def test_flow_user_error(hass: HomeAssistant): result["flow_id"], user_input={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} @@ -129,7 +129,7 @@ async def test_flow_user_no_port_list(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -141,7 +141,7 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_confirm" result2 = await hass.config_entries.flow.async_init( @@ -150,5 +150,5 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant): data={}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 0ed4ac35eaa..b88d4d240bd 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -107,7 +107,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -115,5 +115,5 @@ async def test_options_flow(hass): user_input={CONF_SOURCE_1: "one", CONF_SOURCE_4: "", CONF_SOURCE_5: "five"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == {"1": "one", "5": "five"} diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 57ab45d9dbb..23defadde9a 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -372,7 +372,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -380,7 +380,7 @@ async def test_options_flow(hass): user_input={const.CONF_WAIT_FOR_PUSH: False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_WAIT_FOR_PUSH: False, } diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 269a2b8a4c4..6fe38ccf7a1 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -79,13 +79,13 @@ async def test_hassio_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "motionEye"} assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result2.get("type") == data_entry_flow.FlowResultType.FORM assert result2.get("step_id") == "user" assert "flow_id" in result2 @@ -109,7 +109,7 @@ async def test_hassio_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3.get("title") == "Add-on" assert result3.get("data") == { CONF_URL: TEST_URL, @@ -287,7 +287,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert dict(config_entry.data) == {**new_data, CONF_WEBHOOK_ID: "test-webhook-id"} @@ -337,7 +337,7 @@ async def test_duplicate(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_client.async_client_close.called @@ -354,7 +354,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -369,7 +369,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -378,14 +378,14 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result2.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result2.get("type") == data_entry_flow.FlowResultType.ABORT assert result2.get("reason") == "already_in_progress" @@ -397,12 +397,12 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result2.get("type") == data_entry_flow.FlowResultType.FORM assert "flow_id" in result2 mock_client = create_mock_motioneye_client() @@ -426,7 +426,7 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 flows = hass.config_entries.flow.async_progress() @@ -449,7 +449,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -460,7 +460,7 @@ async def test_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -492,7 +492,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -511,7 +511,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert result["data"][CONF_STREAM_URL_TEMPLATE] == "http://moo" diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 6f1b662e282..208a6ce2d61 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -218,7 +218,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -277,7 +277,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -289,7 +289,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -311,7 +311,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", @@ -352,7 +352,7 @@ async def test_disable_birth_will( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -364,7 +364,7 @@ async def test_disable_birth_will( mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -386,7 +386,7 @@ async def test_disable_birth_will( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", @@ -448,7 +448,7 @@ async def test_option_flow_default_suggested_values( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -472,7 +472,7 @@ async def test_option_flow_default_suggested_values( mqtt.CONF_PASSWORD: "p4ss", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: True, @@ -506,11 +506,11 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Test updated default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "another-broker", @@ -529,7 +529,7 @@ async def test_option_flow_default_suggested_values( result["flow_id"], user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: False, @@ -563,7 +563,7 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Make sure all MQTT related jobs are done before ending the test await hass.async_block_till_done() @@ -717,7 +717,7 @@ async def test_try_connection_with_advanced_parameters( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -741,7 +741,7 @@ async def test_try_connection_with_advanced_parameters( mqtt.CONF_PASSWORD: "p4ss", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" # check if the username and password was set from config flow and not from configuration.yaml diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 67274cf1c78..aef0fa41fe5 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -32,7 +32,7 @@ async def test_form_create_entry_without_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -51,7 +51,7 @@ async def test_form_create_entry_without_auth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +62,7 @@ async def test_form_create_entry_with_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -80,7 +80,7 @@ async def test_form_create_entry_with_auth(hass): VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "credentials" result = await hass.config_entries.flow.async_configure( @@ -89,7 +89,7 @@ async def test_form_create_entry_with_auth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -120,7 +120,7 @@ async def test_reauth_successful(hass): data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -128,7 +128,7 @@ async def test_reauth_successful(hass): user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -152,7 +152,7 @@ async def test_reauth_unsuccessful(hass): data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -160,7 +160,7 @@ async def test_reauth_unsuccessful(hass): user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" @@ -189,7 +189,7 @@ async def test_form_with_auth_errors(hass, error): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "credentials" with patch( @@ -244,7 +244,7 @@ async def test_form_abort(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "device_unsupported" @@ -271,7 +271,7 @@ async def test_form_already_configured(hass): {"host": "1.1.1.1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -298,7 +298,7 @@ async def test_zeroconf(hass): if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" assert context["confirm_only"] is True @@ -313,7 +313,7 @@ async def test_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"] == {"host": "10.10.2.3"} assert len(mock_setup_entry.mock_calls) == 1 @@ -339,7 +339,7 @@ async def test_zeroconf_with_auth(hass): if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" @@ -359,7 +359,7 @@ async def test_zeroconf_with_auth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -380,7 +380,7 @@ async def test_zeroconf_host_already_configured(hass): context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -404,5 +404,5 @@ async def test_zeroconf_errors(hass, error): context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 7e187f1e2fd..14571c75588 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -85,7 +85,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -111,7 +111,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -148,7 +148,7 @@ async def test_reauth( new_entry = hass.config_entries.async_get_entry("my_entry") - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert new_entry.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py index f199d2ec7dd..3784304ac8b 100644 --- a/tests/components/nest/test_config_flow_legacy.py +++ b/tests/components/nest/test_config_flow_legacy.py @@ -24,7 +24,7 @@ async def test_abort_if_single_instance_allowed(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -40,14 +40,14 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"flow_impl": "nest"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert ( result["description_placeholders"] @@ -69,7 +69,7 @@ async def test_full_flow_implementation(hass): ) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["tokens"] == {"access_token": "yoo"} assert result["data"]["impl_domain"] == "nest" assert result["title"] == "Nest (via configuration.yaml)" @@ -83,7 +83,7 @@ async def test_not_pick_implementation_if_only_one(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -99,7 +99,7 @@ async def test_abort_if_timeout_generating_auth_url(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -115,7 +115,7 @@ async def test_abort_if_exception_generating_auth_url(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -127,7 +127,7 @@ async def test_verify_code_timeout(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -137,7 +137,7 @@ async def test_verify_code_timeout(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "timeout"} @@ -150,7 +150,7 @@ async def test_verify_code_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -160,7 +160,7 @@ async def test_verify_code_invalid(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "invalid_pin"} @@ -173,7 +173,7 @@ async def test_verify_code_unknown_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -183,7 +183,7 @@ async def test_verify_code_unknown_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "unknown"} @@ -196,7 +196,7 @@ async def test_verify_code_exception(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -206,7 +206,7 @@ async def test_verify_code_exception(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "internal_error"} @@ -220,7 +220,7 @@ async def test_step_import(hass): flow = hass.config_entries.flow.async_progress()[0] result = await hass.config_entries.flow.async_configure(flow["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 30fb5fd3d47..ae13268eda3 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -34,7 +34,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await hass.config_entries.flow.async_init( @@ -50,7 +50,7 @@ async def test_abort_if_existing_entry(hass): type="mock_type", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -142,28 +142,28 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -200,28 +200,28 @@ async def test_option_flow_wrong_coordinates(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -289,7 +289,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -326,7 +326,7 @@ async def test_reauth( new_entry2 = hass.config_entries.async_entries(DOMAIN)[0] - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert new_entry2.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index d46284f5049..69dc57b1d2c 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -115,7 +115,7 @@ async def test_user(hass, service): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -127,7 +127,7 @@ async def test_user(hass, service): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -142,7 +142,7 @@ async def test_user_connect_error(hass, service_failed): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -154,7 +154,7 @@ async def test_user_connect_error(hass, service_failed): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "config"} @@ -164,7 +164,7 @@ async def test_user_incomplete_info(hass, service_incomplete): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -176,7 +176,7 @@ async def test_user_incomplete_info(hass, service_incomplete): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE_INCOMPLETE assert result["data"].get(CONF_HOST) == HOST @@ -198,14 +198,14 @@ async def test_abort_if_already_setup(hass, service): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -231,7 +231,7 @@ async def test_ssdp_already_configured(hass): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -257,7 +257,7 @@ async def test_ssdp_ipv6(hass): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_ipv4_address" @@ -277,13 +277,13 @@ async def test_ssdp(hass, service): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -309,13 +309,13 @@ async def test_ssdp_port_5555(hass, service_5555): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -340,7 +340,7 @@ async def test_options_flow(hass, service): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -350,7 +350,7 @@ async def test_options_flow(hass, service): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 1800, } diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index 0b3ac8a798e..ad17de1c150 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -36,7 +36,7 @@ async def test_form_create_entry(hass): {CONF_API_KEY: "fake_api_key"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "profiles" result = await hass.config_entries.flow.async_configure( @@ -44,7 +44,7 @@ async def test_form_create_entry(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Fake Profile" assert result["data"][CONF_API_KEY] == "fake_api_key" assert result["data"][CONF_PROFILE_ID] == "xyz12" @@ -96,5 +96,5 @@ async def test_form_already_configured(hass): result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nfandroidtv/test_config_flow.py b/tests/components/nfandroidtv/test_config_flow.py index a8b7b5fef53..cfafced5202 100644 --- a/tests/components/nfandroidtv/test_config_flow.py +++ b/tests/components/nfandroidtv/test_config_flow.py @@ -37,7 +37,7 @@ async def test_flow_user(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -62,7 +62,7 @@ async def test_flow_user_already_configured(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -76,7 +76,7 @@ async def test_flow_user_cannot_connect(hass): context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -91,6 +91,6 @@ async def test_flow_user_unknown_error(hass): context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index d7a54ba28fb..9a2b070c20c 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with _patch_glucose_readings(), _patch_server_status(), _patch_async_setup_entry() as mock_setup_entry: @@ -34,7 +34,7 @@ async def test_form(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == SERVER_STATUS.name # pylint: disable=maybe-no-member assert result2["data"] == CONFIG await hass.async_block_till_done() @@ -56,7 +56,7 @@ async def test_user_form_cannot_connect(hass): {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -78,7 +78,7 @@ async def test_user_form_api_key_required(hass): {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -97,7 +97,7 @@ async def test_user_form_unexpected_exception(hass): {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -112,7 +112,7 @@ async def test_user_form_duplicate(hass): context={"source": config_entries.SOURCE_USER}, data=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index 738eb80e190..a93cecdd102 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -58,7 +58,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -73,7 +73,7 @@ async def test_step_user_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -88,7 +88,7 @@ async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_step_user(hass: HomeAssistant) -> None: @@ -105,7 +105,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "NINA" @@ -120,7 +120,7 @@ async def test_step_user_no_selection(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "no_selection"} @@ -139,7 +139,7 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -168,7 +168,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -183,7 +183,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] is None assert dict(config_entry.data) == { @@ -221,7 +221,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -236,7 +236,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "no_selection"} @@ -262,7 +262,7 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -287,7 +287,7 @@ async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: @@ -321,7 +321,7 @@ async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entity_registry: er = er.async_get(hass) entries = er.async_entries_for_config_entry( diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 3016727f7be..9a94efb6968 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -202,7 +202,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { @@ -231,7 +231,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", CONF_HOME_INTERVAL: 5, diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 45bcedde155..92e285ba899 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -15,7 +15,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -45,7 +45,7 @@ async def test_step_reauth(hass, config, config_entry, setup_notion): assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch("homeassistant.components.notion.async_setup_entry", return_value=True): @@ -54,7 +54,7 @@ async def test_step_reauth(hass, config, config_entry, setup_notion): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -64,7 +64,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -73,7 +73,7 @@ async def test_step_user(hass, config, setup_notion): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@host.com" assert result["data"] == { CONF_USERNAME: "user@host.com", diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index 77966bd7e5f..72713e24e99 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -38,7 +38,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -67,7 +67,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -90,7 +90,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -113,7 +113,7 @@ async def test_form_unknown_exception(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -137,7 +137,7 @@ async def test_form_already_configured(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_dhcp_flow(hass): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -168,7 +168,7 @@ async def test_dhcp_flow(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -189,7 +189,7 @@ async def test_dhcp_flow_already_configured(hass): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -200,7 +200,7 @@ async def test_reauth_success(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -216,7 +216,7 @@ async def test_reauth_success(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data[CONF_TOKEN] == "new-token" @@ -228,7 +228,7 @@ async def test_reauth_invalid_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -240,7 +240,7 @@ async def test_reauth_invalid_auth(hass): user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "invalid_auth"} @@ -252,7 +252,7 @@ async def test_reauth_cannot_connect(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -264,7 +264,7 @@ async def test_reauth_cannot_connect(hass): user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "cannot_connect"} @@ -276,7 +276,7 @@ async def test_reauth_unknown_exception(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -288,6 +288,6 @@ async def test_reauth_unknown_exception(hass): user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 2261fec3a86..2bb6a5d8286 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form_zeroconf(hass): type="mock_type", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -66,7 +66,7 @@ async def test_form_zeroconf(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "192.168.1.5:1234" assert result2["data"] == { CONF_HOST: "192.168.1.5", @@ -84,7 +84,7 @@ async def test_form_user_one_ups(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -109,7 +109,7 @@ async def test_form_user_one_ups(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -133,7 +133,7 @@ async def test_form_user_multiple_ups(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -156,7 +156,7 @@ async def test_form_user_multiple_ups(hass): ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -171,7 +171,7 @@ async def test_form_user_multiple_ups(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "ups2@1.1.1.1:2222" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -194,7 +194,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -219,7 +219,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -252,7 +252,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch( @@ -272,7 +272,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch( @@ -292,7 +292,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -329,7 +329,7 @@ async def test_abort_if_already_setup(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -368,7 +368,7 @@ async def test_abort_if_already_setup_alias(hass): ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -379,7 +379,7 @@ async def test_abort_if_already_setup_alias(hass): {CONF_ALIAS: "ups1"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -396,14 +396,14 @@ async def test_options_flow(hass): with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 60, } @@ -411,7 +411,7 @@ async def test_options_flow(hass): with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result2 = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -419,7 +419,7 @@ async def test_options_flow(hass): user_input={CONF_SCAN_INTERVAL: 12}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 12, } diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index 7769e082016..e9de98206d1 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -230,7 +230,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_show_ssdp_form(hass: HomeAssistant) -> None: @@ -296,7 +296,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_import_yaml(hass: HomeAssistant) -> None: @@ -328,7 +328,7 @@ async def test_import_yaml(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert "errors" not in result @@ -362,7 +362,7 @@ async def test_import_duplicate_yaml(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(request_app_key.mock_calls) == 0 - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index bd8f32b05bd..f3a60479b46 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -129,7 +129,7 @@ async def test_option_flow(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -137,6 +137,6 @@ async def test_option_flow(hass): user_input={"polling_interval": 9}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"]["polling_interval"] == 9 diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py index e1edfc2a63c..17db65f6b41 100644 --- a/tests/components/ondilo_ico/test_config_flow.py +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -25,7 +25,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index fec1e2b8132..31c2f06f352 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -73,7 +73,7 @@ async def test_flow_discovered_devices(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -91,7 +91,7 @@ async def test_flow_discovered_devices(hass): result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device" assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 3 @@ -99,7 +99,7 @@ async def test_flow_discovered_devices(hass): result["flow_id"], user_input={config_flow.CONF_HOST: f"{URN} ({HOST})"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -116,7 +116,7 @@ async def test_flow_discovered_devices(hass): await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{URN} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: URN, @@ -135,7 +135,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -153,7 +153,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device" assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 2 @@ -162,7 +162,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): user_input={config_flow.CONF_HOST: config_flow.CONF_MANUAL_INPUT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" @@ -174,7 +174,7 @@ async def test_flow_discovered_no_device(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -192,7 +192,7 @@ async def test_flow_discovered_no_device(hass): result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" @@ -216,7 +216,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -235,7 +235,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): ) # It should skip to manual entry if the only devices are already configured - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" result = await hass.config_entries.flow.async_configure( @@ -250,7 +250,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): ) # It should abort if already configured and entered manually - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_flow_manual_entry(hass): @@ -259,7 +259,7 @@ async def test_flow_manual_entry(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -279,7 +279,7 @@ async def test_flow_manual_entry(hass): user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -299,7 +299,7 @@ async def test_flow_manual_entry(hass): await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{NAME} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: NAME, @@ -318,7 +318,7 @@ async def test_option_flow(hass): entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "onvif_devices" result = await hass.config_entries.options.async_configure( @@ -330,7 +330,7 @@ async def test_option_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { config_flow.CONF_EXTRA_ARGUMENTS: "", config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 99133cf17c3..8244ece6b9f 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -204,7 +204,7 @@ async def test_options_migration(hass): entry.entry_id, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -212,7 +212,7 @@ async def test_options_migration(hass): user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_FLOOR_TEMP] is True @@ -243,7 +243,7 @@ async def test_options_form(hass): result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -256,7 +256,7 @@ async def test_options_form(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_HALVES assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -270,7 +270,7 @@ async def test_options_form(hass): result["flow_id"], user_input={CONF_READ_PRECISION: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == 0.0 assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -290,7 +290,7 @@ async def test_options_form(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is False diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 8960d39a5b9..9f51728365b 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -19,7 +19,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -32,7 +32,7 @@ async def test_invalid_api_key(hass, config): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -41,13 +41,13 @@ async def test_options_flow(hass, config_entry): with patch("homeassistant.components.openuv.async_setup_entry", return_value=True): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} @@ -56,13 +56,13 @@ async def test_step_user(hass, config, setup_openuv): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "51.528308, -0.3817765" assert result["data"] == { CONF_API_KEY: "abcde12345", diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 5225aad83cd..12ee849d3d2 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -63,7 +63,7 @@ async def test_form(hass): await hass.async_block_till_done() assert entry.state == ConfigEntryState.NOT_LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -90,14 +90,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_MODE: "daily"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, @@ -109,14 +109,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_MODE: "onecall_daily"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "onecall_daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 967d6cbb8c8..0542f4dc9fc 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -106,7 +106,7 @@ async def test_form_invalid_auth( ) assert result["step_id"] == config_entries.SOURCE_USER - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -131,7 +131,7 @@ async def test_abort_on_duplicate_entry(hass: HomeAssistant) -> None: {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -177,7 +177,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -220,7 +220,7 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -232,7 +232,7 @@ async def test_zeroconf_flow(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -271,7 +271,7 @@ async def test_zeroconf_flow_already_configured(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -295,7 +295,7 @@ async def test_reauth_success(hass): data=mock_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -311,7 +311,7 @@ async def test_reauth_success(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_entry.data["username"] == TEST_EMAIL assert mock_entry.data["password"] == TEST_PASSWORD2 @@ -337,7 +337,7 @@ async def test_reauth_wrong_account(hass): data=mock_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -353,5 +353,5 @@ async def test_reauth_wrong_account(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_wrong_account" diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index a8f0c098aba..5fbd4586d12 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -22,7 +22,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -32,7 +32,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -44,7 +44,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -55,7 +55,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -67,7 +67,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -78,7 +78,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -93,7 +93,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -110,7 +110,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -119,7 +119,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "authorization_error"} @@ -136,7 +136,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -145,7 +145,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "connection_error"} @@ -167,7 +167,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" assert result["errors"] == {"base": "authorization_error"} @@ -184,5 +184,5 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 31a3a327d1c..e6d337f9420 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -64,11 +64,11 @@ async def test_user(hass, webhook_id, secret): flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "OwnTracks" assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID assert result["data"][CONF_SECRET] == SECRET @@ -98,7 +98,7 @@ async def test_abort_if_already_setup(hass): # Should fail, already setup (flow) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -107,7 +107,7 @@ async def test_user_not_supports_encryption(hass, not_supports_encryption): flow = await init_config_flow(hass) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result["description_placeholders"]["secret"] == "Encryption is not supported because nacl is not installed." @@ -165,7 +165,7 @@ async def test_with_cloud_sub(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] assert entry.data["cloudhook"] assert ( @@ -192,5 +192,5 @@ async def test_with_cloud_sub_not_connected(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index ace62195115..c6bade94ea4 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -219,12 +219,12 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_ALLOW_NOTIFY: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_ALLOW_NOTIFY: True} diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index 3ea54cee593..24878a1b701 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -39,7 +39,7 @@ async def test_form(hass, picnic_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -85,7 +85,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -108,7 +108,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -131,7 +131,7 @@ async def test_form_exception(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -158,7 +158,7 @@ async def test_form_already_configured(hass, picnic_api): ) await hass.async_block_till_done() - assert result_configure["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT assert result_configure["reason"] == "already_configured" @@ -177,7 +177,7 @@ async def test_step_reauth(hass, picnic_api): result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["type"] == data_entry_flow.FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -195,7 +195,7 @@ async def test_step_reauth(hass, picnic_api): await hass.async_block_till_done() # Check that the returned flow has type abort because of successful re-authentication - assert result_configure["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT assert result_configure["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -217,7 +217,7 @@ async def test_step_reauth_failed(hass): result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["type"] == data_entry_flow.FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -256,7 +256,7 @@ async def test_step_reauth_different_account(hass, picnic_api): result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["type"] == data_entry_flow.FlowResultType.FORM assert result_init["step_id"] == "user" with patch( diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index ba244138469..b7b39aa50b4 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -304,7 +304,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( @@ -314,7 +314,7 @@ async def test_options(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 10 assert len(mock_setup_entry.mock_calls) == 1 @@ -339,7 +339,7 @@ async def test_options_webhook(hass, webhook_id): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "webhook" assert result["description_placeholders"] == {"webhook_url": ""} @@ -350,7 +350,7 @@ async def test_options_webhook(hass, webhook_id): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 93ea18f21b6..e9587592175 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -48,7 +48,7 @@ async def test_abort_if_no_implementation_registered(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_flows" @@ -58,12 +58,12 @@ async def test_abort_if_already_setup(hass): with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -75,18 +75,18 @@ async def test_full_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "https://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["refresh_args"] == { CONF_CLIENT_ID: "id", CONF_CLIENT_SECRET: "secret", @@ -100,7 +100,7 @@ async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-out flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -112,7 +112,7 @@ async def test_wrong_code_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_error" @@ -121,7 +121,7 @@ async def test_not_pick_implementation_if_only_one(hass): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -130,7 +130,7 @@ async def test_abort_if_timeout_generating_auth_url(hass): flow = init_config_flow(hass, side_effect=asyncio.TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -139,7 +139,7 @@ async def test_abort_if_exception_generating_auth_url(hass): flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -148,5 +148,5 @@ async def test_abort_no_code(hass): flow = init_config_flow(hass) result = await flow.async_step_code() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_code" diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index 71fa76df7ab..0710ff1d26d 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -13,7 +13,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -47,7 +47,7 @@ async def test_valid_credentials(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test-email" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 57e65d15be4..346b9718165 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -103,7 +103,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -111,7 +111,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -121,7 +121,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -131,7 +131,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -144,7 +144,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -152,7 +152,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -163,7 +163,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -174,7 +174,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -196,7 +196,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -204,7 +204,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -215,7 +215,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" # Step Link @@ -226,7 +226,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -249,7 +249,7 @@ async def test_port_bind_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): @@ -257,7 +257,7 @@ async def test_port_bind_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason @@ -269,14 +269,14 @@ async def test_duplicate_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -285,7 +285,7 @@ async def test_duplicate_abort(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -299,14 +299,14 @@ async def test_additional_device(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -322,7 +322,7 @@ async def test_additional_device(hass): result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -336,7 +336,7 @@ async def test_0_pin(hass): context={"source": "creds"}, data={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -348,7 +348,7 @@ async def test_0_pin(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" mock_config = MOCK_CONFIG @@ -372,14 +372,14 @@ async def test_no_devices_found_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]): @@ -387,7 +387,7 @@ async def test_no_devices_found_abort(hass): result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -397,14 +397,14 @@ async def test_manual_mode(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input: manual, results in Step Link. @@ -415,7 +415,7 @@ async def test_manual_mode(hass): result["flow_id"], user_input=MOCK_MANUAL ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -425,7 +425,7 @@ async def test_credential_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None): @@ -433,7 +433,7 @@ async def test_credential_abort(hass): result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "credential_error" @@ -443,7 +443,7 @@ async def test_credential_timeout(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout): @@ -451,7 +451,7 @@ async def test_credential_timeout(hass): result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" assert result["errors"] == {"base": "credential_timeout"} @@ -462,14 +462,14 @@ async def test_wrong_pin_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -483,7 +483,7 @@ async def test_wrong_pin_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "login_failed"} @@ -494,14 +494,14 @@ async def test_device_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -515,7 +515,7 @@ async def test_device_connection_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "cannot_connect"} @@ -526,20 +526,20 @@ async def test_manual_mode_no_ip_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"Config Mode": "Manual Entry"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" assert result["errors"] == {CONF_IP_ADDRESS: "no_ipaddress"} diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index a84adba2a70..7cda5b42fa2 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -43,7 +43,7 @@ MOCK_DATA = {CONF_TOKEN: MOCK_CREDS, "devices": [MOCK_DEVICE]} MOCK_FLOW_RESULT = { "version": VERSION, "handler": DOMAIN, - "type": data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + "type": data_entry_flow.FlowResultType.CREATE_ENTRY, "title": "test_ps4", "data": MOCK_DATA, "options": {}, @@ -125,7 +125,7 @@ async def test_creating_entry_sets_up_media_player(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index db1a72147eb..e67aca154c4 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -43,12 +43,12 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.test") @@ -59,11 +59,11 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert pvpc_aioclient_mock.call_count == 1 # Check removal @@ -75,12 +75,12 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.test") @@ -96,7 +96,7 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): config_entry = current_entries[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 02f873c6a4a..fbc850f8fe1 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -48,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -62,7 +62,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result["title"] == f"QNAP {SYSTEM_BOARD_MOCK[API_RESULT][API_PRODUCT]} {SYSTEM_BOARD_MOCK[API_RESULT][API_MAC_ADDR]}" @@ -159,7 +159,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -206,7 +206,7 @@ async def test_dhcp_flow_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -223,7 +223,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -254,7 +254,7 @@ async def test_dhcp_login_error(hass: HomeAssistant): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index 56a361404f8..862b7b30032 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -47,7 +47,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -72,7 +72,7 @@ async def test_form_unknown_error(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -93,7 +93,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {CONF_HOST: "cannot_connect"} @@ -113,7 +113,7 @@ async def test_import(hass): data={CONF_HOST: "1.2.3.4"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "My Name" assert result["data"] == {CONF_HOST: "1.2.3.4"} assert len(mock_setup_entry.mock_calls) == 1 @@ -131,7 +131,7 @@ async def test_import_cannot_connect(hass): data={CONF_HOST: "1.2.3.4"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -153,7 +153,7 @@ async def test_dhcp_can_confirm(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "host": "1.2.3.4", @@ -171,7 +171,7 @@ async def test_dhcp_can_confirm(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -197,7 +197,7 @@ async def test_dhcp_fails_to_connect(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -226,7 +226,7 @@ async def test_dhcp_already_exists(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -243,7 +243,7 @@ async def test_user_unique_id_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -261,5 +261,5 @@ async def test_user_unique_id_already_exists(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 8b313eb2fb5..deb03d65cb5 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -16,7 +16,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -95,13 +95,13 @@ async def test_options_flow(hass, config, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_ZONE_RUN_TIME: 600} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_ZONE_RUN_TIME: 600} @@ -112,7 +112,7 @@ async def test_show_form(hass): context={"source": config_entries.SOURCE_USER}, data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -123,7 +123,7 @@ async def test_step_user(hass, config, setup_rainmachine): context={"source": config_entries.SOURCE_USER}, data=config, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -158,7 +158,7 @@ async def test_step_homekit_zeroconf_ip_already_exists( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -184,7 +184,7 @@ async def test_step_homekit_zeroconf_ip_change(hass, client, config_entry, sourc ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_IP_ADDRESS] == "192.168.1.2" @@ -213,7 +213,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -231,7 +231,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "12345" assert result2["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -261,7 +261,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass, client): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -281,5 +281,5 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass, client): ), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index 4295f3777d5..ba09a2f6d6b 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -18,7 +18,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -31,7 +31,7 @@ async def test_invalid_place_or_service_id(hass, config): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_place_or_service_id"} @@ -42,13 +42,13 @@ async def test_options_flow(hass, config, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FRIENDLY_NAME: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FRIENDLY_NAME: True} @@ -57,7 +57,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -67,6 +67,6 @@ async def test_step_user(hass, config, setup_recollect_waste): DOMAIN, context={"source": SOURCE_USER}, data=config ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "12345, 12345" assert result["data"] == {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index ec5eae468fc..a269106c159 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -38,7 +38,7 @@ async def test_config_flow_single_account( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} # Failed credentials @@ -55,7 +55,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_credentials"} renault_account = AsyncMock() @@ -82,7 +82,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_1" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -97,7 +97,7 @@ async def test_config_flow_no_account(hass: HomeAssistant, mock_setup_entry: Asy result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} # Account list empty @@ -114,7 +114,7 @@ async def test_config_flow_no_account(hass: HomeAssistant, mock_setup_entry: Asy }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "kamereon_no_account" assert len(mock_setup_entry.mock_calls) == 0 @@ -127,7 +127,7 @@ async def test_config_flow_multiple_accounts( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} renault_account_1 = RenaultAccount( @@ -153,7 +153,7 @@ async def test_config_flow_multiple_accounts( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "kamereon" # Account selected @@ -161,7 +161,7 @@ async def test_config_flow_multiple_accounts( result["flow_id"], user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_2" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -179,7 +179,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant, mock_setup_entry: Asyn result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} renault_account = RenaultAccount( @@ -199,7 +199,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant, mock_setup_entry: Asyn }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -220,7 +220,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result["errors"] == {} @@ -234,7 +234,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): user_input={CONF_PASSWORD: "any"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result2["errors"] == {"base": "invalid_credentials"} @@ -245,5 +245,5 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): user_input={CONF_PASSWORD: "any"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 2c695d71d2e..23ce5d1393f 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -314,7 +314,7 @@ async def test_options_global(hass): user_input={"automatic_add": True, "protocols": SOME_PROTOCOLS}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -349,7 +349,7 @@ async def test_no_protocols(hass): user_input={"automatic_add": False, "protocols": []}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -404,7 +404,7 @@ async def test_options_add_device(hass): result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -558,7 +558,7 @@ async def test_options_replace_sensor_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -691,7 +691,7 @@ async def test_options_replace_control_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -772,7 +772,7 @@ async def test_options_add_and_configure_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -816,7 +816,7 @@ async def test_options_add_and_configure_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -896,7 +896,7 @@ async def test_options_configure_rfy_cover_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index dfd182d4a24..8dd7c4bf7f7 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -165,14 +165,14 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=TEST_OPTIONS, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "risco_to_ha" result = await hass.config_entries.options.async_configure( @@ -180,7 +180,7 @@ async def test_options_flow(hass): user_input=TEST_RISCO_TO_HA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ha_to_risco" with patch("homeassistant.components.risco.async_setup_entry", return_value=True): @@ -189,7 +189,7 @@ async def test_options_flow(hass): user_input=TEST_HA_TO_RISCO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { **TEST_OPTIONS, "risco_states_to_ha": TEST_RISCO_TO_HA, diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index de66a1ae3b7..78834ed955a 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -128,7 +128,7 @@ async def test_form_user_discovery_and_password_fetch(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -137,7 +137,7 @@ async def test_form_user_discovery_and_password_fetch(hass): {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -157,7 +157,7 @@ async def test_form_user_discovery_and_password_fetch(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -184,7 +184,7 @@ async def test_form_user_discovery_skips_known(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -204,7 +204,7 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured(ha ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -213,7 +213,7 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured(ha {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -233,7 +233,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -242,7 +242,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -255,7 +255,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] is None with patch( @@ -274,7 +274,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -302,7 +302,7 @@ async def test_form_user_discover_fails_aborts_already_configured(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -311,7 +311,7 @@ async def test_form_user_discover_fails_aborts_already_configured(hass): {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -328,7 +328,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -337,7 +337,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -351,7 +351,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -372,7 +372,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -384,7 +384,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -403,7 +403,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -433,7 +433,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -445,7 +445,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -471,7 +471,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -504,7 +504,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -516,7 +516,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -542,7 +542,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -563,7 +563,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -572,7 +572,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -599,7 +599,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -631,7 +631,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "link" assert result["description_placeholders"] == {"name": "robot_name"} @@ -652,7 +652,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "robot_name" assert result2["result"].unique_id == "BLID" assert result2["data"] == { @@ -684,7 +684,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -693,7 +693,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): {}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -705,7 +705,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] is None with patch( @@ -724,7 +724,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -757,7 +757,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -769,7 +769,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -788,7 +788,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 7cc37bc73cd..3e5bce8c1c2 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -185,7 +185,7 @@ async def test_duplicate_config(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index bc72dff2535..1444d82cc5b 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -55,7 +55,7 @@ async def test_create_entry(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "edc3eee7330e" assert result2["data"] == { CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", @@ -93,7 +93,7 @@ async def test_import_flow(hass) -> None: data=VALID_CONFIG_OLD, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "edc3eee7330e" assert result["data"][CONF_NAME] == "Sabnzbd" assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0" diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 145bcbb3566..f47fdef0994 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -75,7 +75,7 @@ async def test_form(hass, gen): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -93,7 +93,7 @@ async def test_title_without_name(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} settings = MOCK_SETTINGS.copy() @@ -123,7 +123,7 @@ async def test_title_without_name(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "shelly1pm-12345" assert result2["data"] == { "host": "1.1.1.1", @@ -148,7 +148,7 @@ async def test_form_auth(hass, test_data): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -160,7 +160,7 @@ async def test_form_auth(hass, test_data): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -191,7 +191,7 @@ async def test_form_auth(hass, test_data): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", @@ -221,7 +221,7 @@ async def test_form_errors_get_info(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -248,7 +248,7 @@ async def test_form_missing_model_key(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -257,7 +257,7 @@ async def test_form_missing_model_key_auth_enabled(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -269,7 +269,7 @@ async def test_form_missing_model_key_auth_enabled(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -286,7 +286,7 @@ async def test_form_missing_model_key_auth_enabled(hass): result2["flow_id"], {"password": "1234"} ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -311,14 +311,14 @@ async def test_form_missing_model_key_zeroconf(hass, caplog): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "firmware_not_fully_provisioned"} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -342,7 +342,7 @@ async def test_form_errors_test_connection(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -367,7 +367,7 @@ async def test_form_already_configured(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -416,7 +416,7 @@ async def test_user_setup_ignored_device(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Test config entry got updated with latest IP assert entry.data["host"] == "1.1.1.1" @@ -439,7 +439,7 @@ async def test_form_firmware_unsupported(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "unsupported_firmware" @@ -482,7 +482,7 @@ async def test_form_auth_errors_test_connection_gen1(hass, error): result2["flow_id"], {"username": "test username", "password": "test password"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -524,7 +524,7 @@ async def test_form_auth_errors_test_connection_gen2(hass, error): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "test password"} ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -548,7 +548,7 @@ async def test_zeroconf(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -569,7 +569,7 @@ async def test_zeroconf(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -614,7 +614,7 @@ async def test_zeroconf_sleeping_device(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -634,7 +634,7 @@ async def test_zeroconf_sleeping_device(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -677,7 +677,7 @@ async def test_zeroconf_sleeping_device_error(hass, error): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -698,7 +698,7 @@ async def test_zeroconf_already_configured(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -717,7 +717,7 @@ async def test_zeroconf_firmware_unsupported(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unsupported_firmware" @@ -729,7 +729,7 @@ async def test_zeroconf_cannot_connect(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -745,7 +745,7 @@ async def test_zeroconf_require_auth(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -768,7 +768,7 @@ async def test_zeroconf_require_auth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", diff --git a/tests/components/shopping_list/test_config_flow.py b/tests/components/shopping_list/test_config_flow.py index dfc23e18504..552bf19bcd1 100644 --- a/tests/components/shopping_list/test_config_flow.py +++ b/tests/components/shopping_list/test_config_flow.py @@ -11,7 +11,7 @@ async def test_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_user(hass): @@ -21,7 +21,7 @@ async def test_user(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -32,5 +32,5 @@ async def test_user_confirm(hass): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].data == {} diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index 6b1517601f4..85992d0d811 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -160,7 +160,9 @@ async def test_form_start_account(hass, flow_at_add_account_step): async def test_create(hass, entry_with_basic_config): """Test we create a entry through the form.""" - assert entry_with_basic_config["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + entry_with_basic_config["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + ) assert ( entry_with_basic_config["title"] == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" @@ -173,7 +175,7 @@ async def test_create_additional_account(hass, entry_with_additional_account_con """Test we create a config with two accounts.""" assert ( entry_with_additional_account_config["type"] - == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + == data_entry_flow.FlowResultType.CREATE_ENTRY ) assert ( entry_with_additional_account_config["title"] @@ -314,14 +316,14 @@ async def test_options_basic(hass): ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" assert result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], BASIC_OPTIONS ) - assert updated["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert updated["data"] == { CONF_ACCOUNTS: {BASIC_CONFIG[CONF_ACCOUNT]: BASIC_OPTIONS} } @@ -339,13 +341,13 @@ async def test_options_additional(hass): ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" assert not result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], BASIC_OPTIONS ) - assert updated["type"] == data_entry_flow.RESULT_TYPE_FORM + assert updated["type"] == data_entry_flow.FlowResultType.FORM assert updated["step_id"] == "options" assert updated["last_step"] diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py index 4636df6b28f..6c37fb7ffe6 100644 --- a/tests/components/simplepush/test_config_flow.py +++ b/tests/components/simplepush/test_config_flow.py @@ -45,7 +45,7 @@ async def test_flow_successful(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == MOCK_CONFIG @@ -61,7 +61,7 @@ async def test_flow_with_password(hass: HomeAssistant) -> None: result["flow_id"], user_input=mock_config_pass, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == mock_config_pass @@ -84,7 +84,7 @@ async def test_flow_user_device_key_already_configured(hass: HomeAssistant) -> N result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -109,7 +109,7 @@ async def test_flow_user_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_error_on_connection_failure(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -139,6 +139,6 @@ async def test_flow_import(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == MOCK_CONFIG diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 2e0b85bc6c6..9211ec8ea28 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -25,18 +25,18 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -47,13 +47,13 @@ async def test_options_flow(hass, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_CODE: "4321"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_CODE: "4321"} @@ -72,18 +72,18 @@ async def test_step_reauth( DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=reauth_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 2 @@ -112,12 +112,12 @@ async def test_step_reauth_errors(hass, config, error_string, exc, reauth_config DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=reauth_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": error_string} @@ -149,18 +149,18 @@ async def test_step_reauth_from_scratch( DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -186,12 +186,12 @@ async def test_step_user_errors(hass, credentials_config, error_string, exc): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error_string} @@ -205,7 +205,7 @@ async def test_step_user_email_2fa( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Patch API.async_verify_2fa_email to first return pending, then return all done: api.async_verify_2fa_email.side_effect = [Verify2FAPending, None] @@ -213,10 +213,10 @@ async def test_step_user_email_2fa( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -236,7 +236,7 @@ async def test_step_user_email_2fa_timeout( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Patch API.async_verify_2fa_email to return pending: api.async_verify_2fa_email.side_effect = Verify2FAPending @@ -244,14 +244,14 @@ async def test_step_user_email_2fa_timeout( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE assert result["step_id"] == "email_2fa_error" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "email_2fa_timed_out" @@ -263,18 +263,18 @@ async def test_step_user_sms_2fa( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) @@ -300,13 +300,13 @@ async def test_step_user_sms_2fa_errors( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Simulate entering the incorrect SMS code: api.async_verify_2fa_sms.side_effect = InvalidCredentialsError @@ -314,5 +314,5 @@ async def test_step_user_sms_2fa_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"code": error_string} diff --git a/tests/components/slack/test_config_flow.py b/tests/components/slack/test_config_flow.py index 850690783e8..97c8e6ee743 100644 --- a/tests/components/slack/test_config_flow.py +++ b/tests/components/slack/test_config_flow.py @@ -23,7 +23,7 @@ async def test_flow_user( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -42,7 +42,7 @@ async def test_flow_user_already_configured( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -56,7 +56,7 @@ async def test_flow_user_invalid_auth( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -71,7 +71,7 @@ async def test_flow_user_cannot_connect( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -87,7 +87,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -103,7 +103,7 @@ async def test_flow_import( data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -119,7 +119,7 @@ async def test_flow_import_no_name( data=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -136,5 +136,5 @@ async def test_flow_import_already_configured( data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index 3101a7ecdfe..75a2524e2bc 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -46,7 +46,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -67,7 +67,7 @@ async def test_login_failure(hass: HomeAssistant, side_effect, error) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error} @@ -90,7 +90,7 @@ async def test_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] assert len(mock_setup_entry.mock_calls) == 1 @@ -128,5 +128,5 @@ async def test_reauth_password(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 16d330a21a8..b29207afbca 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_user_form(hass): ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_show_user_host_form(hass): @@ -39,14 +39,14 @@ async def test_show_user_host_form(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_show_zeroconf_connection_error_form(hass): @@ -67,14 +67,14 @@ async def test_show_zeroconf_connection_error_form(hass): ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -97,14 +97,14 @@ async def test_show_zeroconf_connection_error_form_next_generation(hass): ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -119,19 +119,19 @@ async def test_connection_error(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_user_local_connection_error(hass): @@ -148,19 +148,19 @@ async def test_user_local_connection_error(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_zeroconf_wrong_mdns(hass): @@ -180,7 +180,7 @@ async def test_zeroconf_wrong_mdns(hass): ) assert result["reason"] == "invalid_mdns" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_full_user_wrong_mdns(hass): @@ -199,18 +199,18 @@ async def test_full_user_wrong_mdns(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_mdns" @@ -239,18 +239,18 @@ async def test_user_device_exists_abort(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -289,7 +289,7 @@ async def test_zeroconf_device_exists_abort(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -310,7 +310,7 @@ async def test_cloud_device_exists_abort(hass): context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -339,7 +339,7 @@ async def test_zeroconf_abort_if_cloud_device_exists(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -371,7 +371,7 @@ async def test_zeroconf_confirm_abort_if_cloud_device_exists(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -396,7 +396,7 @@ async def test_abort_cloud_flow_if_local_device_exists(hass): result["flow_id"], {"environment": ENV_CLOUD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_local_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -479,7 +479,7 @@ async def test_full_zeroconf_flow(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} @@ -487,7 +487,7 @@ async def test_full_zeroconf_flow(hass): result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -513,7 +513,7 @@ async def test_full_user_local_flow(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] is None result = await hass.config_entries.flow.async_configure( @@ -521,12 +521,12 @@ async def test_full_user_local_flow(hass): {"environment": ENV_LOCAL}, ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -555,7 +555,7 @@ async def test_full_zeroconf_flow_next_generation(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} @@ -563,7 +563,7 @@ async def test_full_zeroconf_flow_next_generation(hass): result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "smappee5001000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 420c07d2a04..93b646c44dc 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -27,7 +27,7 @@ async def test_import_shows_user_step(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -52,7 +52,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -60,7 +60,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -69,14 +69,14 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -85,7 +85,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -123,7 +123,7 @@ async def test_entry_created_from_update_event( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -131,7 +131,7 @@ async def test_entry_created_from_update_event( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -140,14 +140,14 @@ async def test_entry_created_from_update_event( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -156,7 +156,7 @@ async def test_entry_created_from_update_event( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -194,7 +194,7 @@ async def test_entry_created_existing_app_new_oauth_client( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -202,7 +202,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -211,14 +211,14 @@ async def test_entry_created_existing_app_new_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -227,7 +227,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -278,7 +278,7 @@ async def test_entry_created_existing_app_copies_oauth_client( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -286,7 +286,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -297,14 +297,14 @@ async def test_entry_created_existing_app_copies_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -313,7 +313,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -370,7 +370,7 @@ async def test_entry_created_with_cloudhook( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -379,7 +379,7 @@ async def test_entry_created_with_cloudhook( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -388,14 +388,14 @@ async def test_entry_created_with_cloudhook( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -404,7 +404,7 @@ async def test_entry_created_with_cloudhook( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -432,7 +432,7 @@ async def test_invalid_webhook_aborts(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_webhook_url" assert result["description_placeholders"][ "webhook_url" @@ -448,7 +448,7 @@ async def test_invalid_token_shows_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -456,7 +456,7 @@ async def test_invalid_token_shows_error(hass): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -465,7 +465,7 @@ async def test_invalid_token_shows_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_invalid_format"} @@ -485,7 +485,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -493,7 +493,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -502,7 +502,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_unauthorized"} @@ -522,7 +522,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -530,7 +530,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -539,7 +539,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_forbidden"} @@ -565,7 +565,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -573,7 +573,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -582,7 +582,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "webhook_error"} @@ -607,7 +607,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -615,7 +615,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -624,7 +624,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -645,7 +645,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -653,7 +653,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -662,7 +662,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -679,7 +679,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -687,7 +687,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -696,7 +696,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -721,7 +721,7 @@ async def test_no_available_locations_aborts( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -729,7 +729,7 @@ async def test_no_available_locations_aborts( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -738,5 +738,5 @@ async def test_no_available_locations_aborts( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_available_locations" diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index c6170afc30e..c388f17c3ba 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -73,14 +73,14 @@ async def test_reauth_success(hass, smarttub_api, account): data=mock_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_EMAIL: "test-email3", CONF_PASSWORD: "test-password3"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_entry.data[CONF_EMAIL] == "test-email3" assert mock_entry.data[CONF_PASSWORD] == "test-password3" @@ -114,12 +114,12 @@ async def test_reauth_wrong_account(hass, smarttub_api, account): data=mock_entry2.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index d0f0ac01235..c23d2578d1c 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -31,7 +31,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" # test with all provided @@ -40,7 +40,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == "solaredge_site_1_2_3" data = result.get("data") @@ -62,7 +62,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> Non context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "already_configured"} @@ -82,7 +82,7 @@ async def test_ignored_entry_does_not_cause_error( context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test" data = result["data"] @@ -102,7 +102,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "site_not_active"} # test with api_failure @@ -112,7 +112,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout @@ -122,7 +122,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} # test with HTTPError @@ -132,5 +132,5 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index ccbc5412562..765f1f569e6 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -63,12 +63,12 @@ async def test_user(hass, test_connect): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # tets with all provided result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -79,19 +79,19 @@ async def test_import(hass, test_connect): # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog" assert result["data"][CONF_HOST] == HOST # import with only name result = await flow.async_step_import({CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == DEFAULT_HOST # import with host and name result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -107,19 +107,19 @@ async def test_abort_if_already_setup(hass, test_connect): result = await flow.async_step_import( {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same HOST and NAME result = await flow.async_step_user({CONF_HOST: HOST, CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "already_configured"} # SHOULD pass, diff HOST (without http://), different NAME result = await flow.async_step_import( {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_7_8_9" assert result["data"][CONF_HOST] == "http://2.2.2.2" @@ -127,6 +127,6 @@ async def test_abort_if_already_setup(hass, test_connect): result = await flow.async_step_import( {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == "http://2.2.2.2" diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 1d00f83a608..66d16ebc480 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): flow = config_flow.SomaFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_import_abort(hass): @@ -27,7 +27,7 @@ async def test_import_abort(hass): flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -37,7 +37,7 @@ async def test_import_create(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_error_status(hass): @@ -46,7 +46,7 @@ async def test_error_status(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "error"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "result_error" @@ -56,7 +56,7 @@ async def test_key_error(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -66,7 +66,7 @@ async def test_exception(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", side_effect=RequestException()): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -77,4 +77,4 @@ async def test_full_flow(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_user({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index d98e7429d8b..1fbb55ca864 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -175,7 +175,7 @@ async def test_options_not_loaded(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT @pytest.mark.parametrize("reversed", [True, False]) @@ -204,7 +204,7 @@ async def test_options_with_targets(hass, reversed): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -212,19 +212,19 @@ async def test_options_with_targets(hass, reversed): user_input={"target_id": "a"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={"reverse": reversed}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM result4 = await hass.config_entries.options.async_configure( result3["flow_id"], user_input={"target_id": None}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_REVERSED_TARGET_IDS: {"a": reversed}, diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 02897c523c1..3b7c86478e0 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -26,10 +26,10 @@ async def test_creating_entry_sets_up_media_player( ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index 7f6f6970c4d..0344c09631b 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -21,13 +21,13 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: @@ -42,7 +42,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -54,7 +54,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SERVER_ID: "1", @@ -67,7 +67,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: # test setting server name to "*Auto Detect" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -79,7 +79,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "*Auto Detect", CONF_SERVER_ID: None, @@ -89,7 +89,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: # test setting the option to update periodically result2 = await hass.config_entries.options.async_init(entry.entry_id) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -114,5 +114,5 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index d00ab53645d..ba8707ed43c 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -32,7 +32,7 @@ async def test_user(hass, spider): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -45,7 +45,7 @@ async def test_user(hass, spider): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -72,7 +72,7 @@ async def test_import(hass, spider): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -91,7 +91,7 @@ async def test_abort_if_already_setup(hass, spider): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" # Should fail, config exist (flow) @@ -99,5 +99,5 @@ async def test_abort_if_already_setup(hass, spider): DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 3b1e4851ff1..a47d25aa06d 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -30,14 +30,14 @@ async def test_abort_if_no_configuration(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -49,7 +49,7 @@ async def test_zeroconf_abort_if_existing_entry(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -79,7 +79,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://accounts.spotify.com/authorize" "?response_type=code&client_id=client" @@ -169,7 +169,7 @@ async def test_abort_if_spotify_error( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -305,7 +305,7 @@ async def test_reauth_account_mismatch( spotify_mock.return_value.current_user.return_value = {"id": "fake_id"} result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_account_mismatch" @@ -315,5 +315,5 @@ async def test_abort_if_no_reauth_entry(hass): DOMAIN, context={"source": "reauth_confirm"} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "reauth_account_mismatch" diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 54f8629a980..6eac5298ae4 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -13,7 +13,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -104,7 +104,7 @@ async def test_config(hass): context={"source": config_entries.SOURCE_IMPORT}, data=ENTRY_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -116,5 +116,5 @@ async def test_integration_already_configured(hass): result = await hass.config_entries.flow.async_init( SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index c4504bf1641..51ecd77c508 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -40,7 +40,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == ACCOUNT_NAME_1 assert result["data"] == CONF_DATA assert result["options"] == CONF_OPTIONS @@ -54,7 +54,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -66,7 +66,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -77,7 +77,7 @@ async def test_flow_user_invalid_account(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_account" @@ -89,7 +89,7 @@ async def test_flow_user_unknown(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -102,7 +102,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -119,20 +119,20 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" new_conf = CONF_DATA | {CONF_API_KEY: "1234567890"} result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf @@ -145,7 +145,7 @@ async def test_flow_import(hass: HomeAssistant) -> None: context={"source": SOURCE_IMPORT}, data=CONF_IMPORT_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == ACCOUNT_NAME_1 assert result["data"] == CONF_DATA assert result["options"] == CONF_IMPORT_OPTIONS @@ -161,7 +161,7 @@ async def test_flow_import_already_configured(hass: HomeAssistant) -> None: context={"source": SOURCE_IMPORT}, data=CONF_IMPORT_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -173,7 +173,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -182,7 +182,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS_2 assert len(er.async_get(hass).entities) == 2 @@ -195,7 +195,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -203,7 +203,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: user_input={CONF_ACCOUNTS: []}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ACCOUNTS: {}} assert len(er.async_get(hass).entities) == 0 @@ -215,7 +215,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: servicemock.side_effect = steam.api.HTTPTimeoutError result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -224,7 +224,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS @@ -234,7 +234,7 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: with patch_interface_private(): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -243,5 +243,5 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py index 80656c75990..6318f4da92e 100644 --- a/tests/components/syncthing/test_config_flow.py +++ b/tests/components/syncthing/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_setup_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "user" @@ -52,7 +52,7 @@ async def test_flow_successful(hass): CONF_VERIFY_SSL: VERIFY_SSL, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:8384" assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_URL] == URL @@ -74,7 +74,7 @@ async def test_flow_already_configured(hass): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -88,7 +88,7 @@ async def test_flow_invalid_auth(hass): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["token"] == "invalid_auth" @@ -102,5 +102,5 @@ async def test_flow_cannot_connect(hass): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 7a91ff7a735..40ed12d0d0b 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -43,7 +43,7 @@ async def test_show_setup_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -65,7 +65,7 @@ async def test_already_configured_by_url(hass, aioclient_mock): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_NAME] == FIXTURE_USER_INPUT[CONF_NAME] assert result["result"].unique_id == udn @@ -80,7 +80,7 @@ async def test_syncthru_not_supported(hass): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "syncthru_not_supported"} @@ -96,7 +96,7 @@ async def test_unknown_state(hass): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "unknown_state"} @@ -115,7 +115,7 @@ async def test_success(hass, aioclient_mock): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -144,7 +144,7 @@ async def test_ssdp(hass, aioclient_mock): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" assert CONF_URL in result["data_schema"].schema for k in result["data_schema"].schema: diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 8cec30abefa..015c3a2ab16 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -120,7 +120,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -136,7 +136,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -163,7 +163,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -185,7 +185,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2sa" # Failed the first time because was too slow to enter the code @@ -195,7 +195,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_OTP_CODE: "000000"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2sa" assert result["errors"] == {CONF_OTP_CODE: "otp_failed"} @@ -206,7 +206,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): result["flow_id"], {CONF_OTP_CODE: "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -226,7 +226,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -242,7 +242,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -288,7 +288,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -298,7 +298,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -323,7 +323,7 @@ async def test_reconfig_user(hass: HomeAssistant, service: MagicMock): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -338,7 +338,7 @@ async def test_login_failed(hass: HomeAssistant, service: MagicMock): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_USERNAME: "invalid_auth"} @@ -354,7 +354,7 @@ async def test_connection_failed(hass: HomeAssistant, service: MagicMock): data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -368,7 +368,7 @@ async def test_unknown_failed(hass: HomeAssistant, service: MagicMock): data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -379,7 +379,7 @@ async def test_missing_data_after_login(hass: HomeAssistant, service_failed: Mag context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "missing_data"} @@ -399,7 +399,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -407,7 +407,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == "192.168.1.5" assert result["data"][CONF_HOST] == "192.168.1.5" @@ -450,7 +450,7 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -482,7 +482,7 @@ async def test_skip_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -514,7 +514,7 @@ async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -535,7 +535,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): assert config_entry.options == {} result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # Scan interval @@ -544,7 +544,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL assert config_entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT assert config_entry.options[CONF_SNAPSHOT_QUALITY] == DEFAULT_SNAPSHOT_QUALITY @@ -555,7 +555,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2, CONF_TIMEOUT: 30, CONF_SNAPSHOT_QUALITY: 0}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 assert config_entry.options[CONF_TIMEOUT] == 30 assert config_entry.options[CONF_SNAPSHOT_QUALITY] == 0 diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index db373f41656..da2916b8c81 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -52,7 +52,7 @@ async def test_reauth_triggered(hass: HomeAssistant): side_effect=SynologyDSMLoginInvalidException(USERNAME), ), patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth", - return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + return_value={"type": data_entry_flow.FlowResultType.FORM}, ) as mock_async_step_reauth: entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 515146bc16c..45131353550 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -102,7 +102,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -112,7 +112,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch( @@ -129,7 +129,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "test-bridge" assert result2["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch( @@ -153,7 +153,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -164,7 +164,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -178,7 +178,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -189,7 +189,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -203,7 +203,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -214,7 +214,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -228,7 +228,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -239,7 +239,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -253,7 +253,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -264,7 +264,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -278,7 +278,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -289,7 +289,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -303,7 +303,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "invalid_auth"} @@ -314,7 +314,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch( @@ -326,7 +326,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -337,7 +337,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -351,7 +351,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -367,7 +367,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -384,7 +384,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -399,7 +399,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -416,7 +416,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == FIXTURE_ZEROCONF_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -431,7 +431,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] with patch( @@ -443,7 +443,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -457,5 +457,5 @@ async def test_zeroconf_bad_zeroconf_info(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF_BAD, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index d37e4401275..d846f0915d0 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -32,7 +32,7 @@ async def test_flow_user_single_instance_allowed(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_IMPORT_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -41,7 +41,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -52,7 +52,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -64,7 +64,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -75,7 +75,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -87,7 +87,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -98,7 +98,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -110,7 +110,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -121,7 +121,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -141,7 +141,7 @@ async def test_flow_reauth( }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -156,7 +156,7 @@ async def test_flow_reauth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == CONF_DATA assert len(mock_entry.mock_calls) == 1 @@ -182,7 +182,7 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -191,5 +191,5 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index 7417c87c229..ba233d04a78 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -62,12 +62,12 @@ async def test_abort_if_already_setup(hass): with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -76,16 +76,16 @@ async def test_full_flow_implementation(hass, mock_tellduslive): flow = init_config_flow(hass) flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert len(flow._hosts) == 2 result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"host": "localhost"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "auth_url": "https://example.com", @@ -93,7 +93,7 @@ async def test_full_flow_implementation(hass, mock_tellduslive): } result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -105,7 +105,7 @@ async def test_step_import(hass, mock_tellduslive): flow = init_config_flow(hass) result = await flow.async_step_import({CONF_HOST: DOMAIN, KEY_SCAN_INTERVAL: 0}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -116,7 +116,7 @@ async def test_step_import_add_host(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -127,7 +127,7 @@ async def test_step_import_no_config_file(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -142,7 +142,7 @@ async def test_step_import_load_json_matching_host(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "Cloud API", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -157,7 +157,7 @@ async def test_step_import_load_json(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -171,7 +171,7 @@ async def test_step_disco_no_local_api(hass, mock_tellduslive): flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert len(flow._hosts) == 1 @@ -182,7 +182,7 @@ async def test_step_auth(hass, mock_tellduslive): await flow.async_step_auth() result = await flow.async_step_auth(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API" assert result["data"]["host"] == "Cloud API" assert result["data"]["scan_interval"] == 60 @@ -199,7 +199,7 @@ async def test_wrong_auth_flow_implementation(hass, mock_tellduslive): await flow.async_step_auth() result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"]["base"] == "invalid_auth" @@ -209,7 +209,7 @@ async def test_not_pick_host_if_only_one(hass, mock_tellduslive): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -218,7 +218,7 @@ async def test_abort_if_timeout_generating_auth_url(hass, mock_tellduslive): flow = init_config_flow(hass, side_effect=asyncio.TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -228,7 +228,7 @@ async def test_abort_no_auth_url(hass, mock_tellduslive): flow._get_auth_url = Mock(return_value=False) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -237,7 +237,7 @@ async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive): flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index 7c623de4ded..9200ed3f382 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -15,7 +15,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -35,7 +35,7 @@ async def test_errors(hass, config, err, err_string): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": err_string} @@ -44,7 +44,7 @@ async def test_step_import(hass, config, setup_tile): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@host.com" assert result["data"] == { CONF_USERNAME: "user@host.com", @@ -60,13 +60,13 @@ async def test_step_reauth(hass, config, config_entry, setup_tile): assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -76,13 +76,13 @@ async def test_step_user(hass, config, setup_tile): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@host.com" assert result["data"] == { CONF_USERNAME: "user@host.com", diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py index ca888210a46..77af05cdc7d 100644 --- a/tests/components/tomorrowio/test_config_flow.py +++ b/tests/components/tomorrowio/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -52,7 +52,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_API_KEY] == API_KEY @@ -77,7 +77,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -85,7 +85,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_NAME] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_API_KEY] == API_KEY @@ -111,7 +111,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -143,7 +143,7 @@ async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -159,7 +159,7 @@ async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "rate_limited"} @@ -175,7 +175,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -199,14 +199,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMESTEP: 1} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMESTEP] == 1 assert entry.options[CONF_TIMESTEP] == 1 @@ -256,13 +256,13 @@ async def test_import_flow_v3( data=old_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "this is a test"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_API_KEY: "this is a test", CONF_LOCATION: { diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 826df81066b..3d7a0613269 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -37,7 +37,7 @@ async def test_abort_if_no_configuration(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -51,7 +51,7 @@ async def test_full_flow_implementation( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # pylint: disable=protected-access @@ -67,7 +67,7 @@ async def test_full_flow_implementation( result["flow_id"], {"implementation": "eneco"} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result2["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result2["url"] == ( "https://api.toon.eu/authorize" "?response_type=code&client_id=client" @@ -141,7 +141,7 @@ async def test_no_agreements( with patch("toonapi.Toon.agreements", return_value=[]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "no_agreements" @@ -185,7 +185,7 @@ async def test_multiple_agreements( ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "agreement" result4 = await hass.config_entries.flow.async_configure( @@ -232,7 +232,7 @@ async def test_agreement_already_set_up( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -271,7 +271,7 @@ async def test_toon_abort( with patch("toonapi.Toon.agreements", side_effect=ToonError): result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "connection_error" @@ -285,7 +285,7 @@ async def test_import(hass, current_request_with_host): DOMAIN, context={"source": SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -333,7 +333,7 @@ async def test_import_migration( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 78b121dda77..5e5124db71c 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -38,7 +38,7 @@ async def test_user(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -65,7 +65,7 @@ async def test_user_show_locations(hass): ) # first it should show the locations form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "locations" # client should have sent four requests for init assert mock_request.call_count == 4 @@ -75,7 +75,7 @@ async def test_user_show_locations(hass): result["flow_id"], user_input={CONF_USERCODES: "bad"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "locations" # client should have sent 5th request to validate usercode assert mock_request.call_count == 5 @@ -85,7 +85,7 @@ async def test_user_show_locations(hass): result2["flow_id"], user_input={CONF_USERCODES: "7890"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # client should have sent another request to validate usercode assert mock_request.call_count == 6 @@ -106,7 +106,7 @@ async def test_abort_if_already_setup(hass): data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -122,7 +122,7 @@ async def test_login_failed(hass): data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -138,7 +138,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -152,7 +152,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -162,7 +162,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_no_locations(hass): context={"source": SOURCE_USER}, data=CONFIG_DATA_NO_USERCODES, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_locations" await hass.async_block_till_done() @@ -221,14 +221,14 @@ async def test_options_flow(hass: HomeAssistant): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={AUTO_BYPASS: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {AUTO_BYPASS: True} await hass.async_block_till_done() diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 2d0140db815..830670efc11 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -63,10 +63,10 @@ async def webhook_id_fixture(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 0ac431a7f35..e6de115c1ca 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -35,7 +35,7 @@ async def test_already_paired(hass, mock_entry_setup): result["flow_id"], {"host": "123.123.123.123", "security_code": "abcd"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_authenticate"} @@ -53,7 +53,7 @@ async def test_user_connection_successful(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].data == { "host": "123.123.123.123", "gateway_id": "bla", @@ -74,7 +74,7 @@ async def test_user_connection_timeout(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "timeout"} @@ -92,7 +92,7 @@ async def test_user_connection_bad_key(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"security_code": "invalid_security_code"} @@ -120,7 +120,7 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "homekit-id" assert result["result"].data == { "host": "123.123.123.123", @@ -149,7 +149,7 @@ async def test_discovery_duplicate_aborted(hass): ), ) - assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["type"] == data_entry_flow.FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.data["host"] == "new-host" @@ -165,7 +165,7 @@ async def test_import_duplicate_aborted(hass): data={"host": "some-host"}, ) - assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["type"] == data_entry_flow.FlowResultType.ABORT assert flow["reason"] == "already_configured" @@ -185,7 +185,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( "tradfri", @@ -201,7 +201,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): ), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT async def test_discovery_updates_unique_id(hass): @@ -226,7 +226,7 @@ async def test_discovery_updates_unique_id(hass): ), ) - assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["type"] == data_entry_flow.FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.unique_id == "homekit-id" diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 7588736e997..24df92f536e 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -89,7 +89,7 @@ async def test_flow_user_config(hass, api): result = await hass.config_entries.flow.async_init( transmission.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -101,7 +101,7 @@ async def test_flow_required_fields(hass, api): data={CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -116,7 +116,7 @@ async def test_flow_all_provided(hass, api): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -137,10 +137,10 @@ async def test_options(hass): options_flow = flow.async_get_options_flow(entry) result = await options_flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 10 @@ -171,7 +171,7 @@ async def test_host_already_configured(hass, api): context={"source": config_entries.SOURCE_USER}, data=mock_entry_unique_port, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY mock_entry_unique_host = MOCK_ENTRY.copy() mock_entry_unique_host[CONF_HOST] = "192.168.1.101" @@ -181,7 +181,7 @@ async def test_host_already_configured(hass, api): context={"source": config_entries.SOURCE_USER}, data=mock_entry_unique_host, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_name_already_configured(hass, api): @@ -218,7 +218,7 @@ async def test_error_on_wrong_credentials(hass, auth_error): CONF_PORT: PORT, } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", @@ -238,7 +238,7 @@ async def test_error_on_connection_failure(hass, conn_error): CONF_PORT: PORT, } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -255,7 +255,7 @@ async def test_error_on_unknown_error(hass, unknown_error): CONF_PORT: PORT, } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 8490f7541eb..d4fe42f10c7 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -14,10 +14,10 @@ async def test_config_flow_registers_webhook(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "twilio", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] twilio_events = [] diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index e774c5a551d..a1f6f3d4b02 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -95,7 +95,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "unifi", @@ -135,7 +135,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Site name" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -161,7 +161,7 @@ async def test_flow_works_negative_discovery(hass, aioclient_mock, mock_discover UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "", @@ -178,7 +178,7 @@ async def test_flow_multiple_sites(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -212,7 +212,7 @@ async def test_flow_multiple_sites(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "site" assert result["data_schema"]({"site": "1"}) assert result["data_schema"]({"site": "2"}) @@ -226,7 +226,7 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.clear_requests() @@ -261,7 +261,7 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -281,7 +281,7 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -315,7 +315,7 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "configuration_updated" @@ -325,7 +325,7 @@ async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -342,7 +342,7 @@ async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "faulty_credentials"} @@ -352,7 +352,7 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -369,7 +369,7 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "service_unavailable"} @@ -389,7 +389,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): data=config_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER aioclient_mock.clear_requests() @@ -424,7 +424,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_HOST] == "1.2.3.4" assert config_entry.data[CONF_USERNAME] == "new_name" @@ -447,7 +447,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device_tracker" assert not result["last_step"] assert set( @@ -465,7 +465,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "client_control" assert not result["last_step"] @@ -478,7 +478,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "statistics_sensors" assert result["last_step"] @@ -490,7 +490,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACK_CLIENTS: False, CONF_TRACK_WIRED_CLIENTS: False, @@ -521,7 +521,7 @@ async def test_simple_option_flow(hass, aioclient_mock): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "simple_options" assert result["last_step"] @@ -534,7 +534,7 @@ async def test_simple_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, diff --git a/tests/components/upcloud/test_config_flow.py b/tests/components/upcloud/test_config_flow.py index 7fce853b5d3..4bbc7c51b9d 100644 --- a/tests/components/upcloud/test_config_flow.py +++ b/tests/components/upcloud/test_config_flow.py @@ -26,7 +26,7 @@ async def test_show_set_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -37,7 +37,7 @@ async def test_connection_error(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -56,7 +56,7 @@ async def test_login_error(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -68,7 +68,7 @@ async def test_success(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -82,7 +82,7 @@ async def test_options(hass): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 66d84fe0862..e89b8274c18 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -45,7 +45,7 @@ async def test_flow_ssdp(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -53,7 +53,7 @@ async def test_flow_ssdp(hass: HomeAssistant): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -81,7 +81,7 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "incomplete_discovery" @@ -102,7 +102,7 @@ async def test_flow_ssdp_non_igd_device(hass: HomeAssistant): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "non_igd_device" @@ -120,7 +120,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -128,7 +128,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -167,7 +167,7 @@ async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "config_entry_updated" @@ -207,7 +207,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs(hass: HomeAssistan context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # UDN + ST different: New discovery via step ssdp. @@ -225,7 +225,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs(hass: HomeAssistan context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" @@ -256,7 +256,7 @@ async def test_flow_ssdp_discovery_changed_location(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test if location is updated. @@ -285,7 +285,7 @@ async def test_flow_ssdp_discovery_ignored_entry(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -316,7 +316,7 @@ async def test_flow_ssdp_discovery_changed_udn_ignored_entry(hass: HomeAssistant context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "discovery_ignored" @@ -332,7 +332,7 @@ async def test_flow_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Confirmed via step user. @@ -340,7 +340,7 @@ async def test_flow_user(hass: HomeAssistant): result["flow_id"], user_input={"unique_id": TEST_USN}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -362,5 +362,5 @@ async def test_flow_user_no_discovery(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 960eedcbd01..207f745e495 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -66,20 +66,20 @@ async def test_user(hass: HomeAssistant): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user( {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "velbus_test_serial" assert result["data"][CONF_PORT] == PORT_SERIAL result = await flow.async_step_user( {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "velbus_test_tcp" assert result["data"][CONF_PORT] == PORT_TCP @@ -92,13 +92,13 @@ async def test_user_fail(hass: HomeAssistant): result = await flow.async_step_user( {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PORT: "cannot_connect"} result = await flow.async_step_user( {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PORT: "cannot_connect"} @@ -108,7 +108,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): flow = init_config_flow(hass) result = await flow.async_step_user({CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"port": "already_configured"} @@ -121,14 +121,14 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # test an already configured discovery entry = MockConfigEntry( @@ -141,7 +141,7 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -154,5 +154,5 @@ async def test_flow_usb_failed(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 780583e38ab..8448bddb288 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -138,7 +138,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -148,7 +148,7 @@ async def test_options(hass): CONF_EXCLUDE: "8,9;10 11 12_13bb14", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index cd68a0b5877..a1f9914ed67 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -17,7 +17,7 @@ async def test_abort_already_setup(hass): ) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -29,7 +29,7 @@ async def test_invalid_login_error(hass): with patch("pyvesync.vesync.VeSync.login", return_value=False): result = await flow.async_step_user(user_input=test_dict) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -38,11 +38,11 @@ async def test_config_flow_user_input(hass): flow = config_flow.VeSyncFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch("pyvesync.vesync.VeSync.login", return_value=True): result = await flow.async_step_user( {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == "user" assert result["data"][CONF_PASSWORD] == "pass" diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index 3096f8a492c..d2852b33606 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert len(result["errors"]) == 0 with patch( @@ -38,7 +38,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "ViCare" assert result2["data"] == ENTRY_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -64,7 +64,7 @@ async def test_invalid_login(hass) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -81,7 +81,7 @@ async def test_form_dhcp(hass): macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -102,7 +102,7 @@ async def test_form_dhcp(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "ViCare" assert result2["data"] == ENTRY_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -125,7 +125,7 @@ async def test_dhcp_single_instance_allowed(hass): macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -141,5 +141,5 @@ async def test_user_input_single_instance_allowed(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index 571399949b4..308431782f3 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -15,7 +15,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch("vilfo.Client.ping", return_value=None), patch( @@ -29,7 +29,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "testadmin.vilfo.com" assert result2["data"] == { "host": "testadmin.vilfo.com", @@ -56,7 +56,7 @@ async def test_form_invalid_auth(hass): {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -74,7 +74,7 @@ async def test_form_cannot_connect(hass): {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), patch( @@ -85,7 +85,7 @@ async def test_form_cannot_connect(hass): {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -128,8 +128,8 @@ async def test_form_already_configured(hass): {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, ) - assert first_flow_result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert second_flow_result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert first_flow_result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert second_flow_result2["type"] == data_entry_flow.FlowResultType.ABORT assert second_flow_result2["reason"] == "already_configured" diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 3250163ef8e..dd338be1321 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -66,14 +66,14 @@ async def test_user_flow_minimum_fields( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -91,14 +91,14 @@ async def test_user_flow_all_fields( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -117,19 +117,19 @@ async def test_speaker_options_flow( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -145,12 +145,12 @@ async def test_tv_options_flow_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -160,7 +160,7 @@ async def test_tv_options_flow_no_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -176,12 +176,12 @@ async def test_tv_options_flow_with_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -191,7 +191,7 @@ async def test_tv_options_flow_with_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -208,13 +208,13 @@ async def test_tv_options_flow_start_with_volume( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init( entry.entry_id, data={CONF_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP} @@ -223,7 +223,7 @@ async def test_tv_options_flow_start_with_volume( result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -233,7 +233,7 @@ async def test_tv_options_flow_start_with_volume( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -260,7 +260,7 @@ async def test_user_host_already_configured( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -284,7 +284,7 @@ async def test_user_serial_number_already_exists( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -296,7 +296,7 @@ async def test_user_error_on_could_not_connect( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -308,7 +308,7 @@ async def test_user_error_on_could_not_connect_invalid_token( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -323,19 +323,19 @@ async def test_user_tv_pairing_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pairing_complete" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -354,7 +354,7 @@ async def test_user_start_pairing_failure( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -370,14 +370,14 @@ async def test_user_invalid_pin( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" assert result["errors"] == {CONF_PIN: "complete_pairing_failed"} @@ -399,7 +399,7 @@ async def test_user_ignore( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_import_flow_minimum_fields( @@ -416,7 +416,7 @@ async def test_import_flow_minimum_fields( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST @@ -436,7 +436,7 @@ async def test_import_flow_all_fields( data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -463,7 +463,7 @@ async def test_import_entity_already_configured( DOMAIN, context={"source": SOURCE_IMPORT}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" @@ -481,7 +481,7 @@ async def test_import_flow_update_options( await hass.async_block_till_done() assert result["result"].options == {CONF_VOLUME_STEP: DEFAULT_VOLUME_STEP} - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_SPEAKER_CONFIG.copy() @@ -492,7 +492,7 @@ async def test_import_flow_update_options( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 @@ -512,7 +512,7 @@ async def test_import_flow_update_name_and_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy() @@ -524,7 +524,7 @@ async def test_import_flow_update_name_and_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.data[CONF_NAME] == NAME2 @@ -546,7 +546,7 @@ async def test_import_flow_update_remove_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS in config_entry.options @@ -559,7 +559,7 @@ async def test_import_flow_update_remove_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" assert CONF_APPS not in config_entry.data assert CONF_APPS not in config_entry.options @@ -576,26 +576,26 @@ async def test_import_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -616,7 +616,7 @@ async def test_import_with_apps_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=import_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Mock inputting info without apps to make sure apps get stored @@ -625,19 +625,19 @@ async def test_import_with_apps_needs_pairing( user_input=_get_config_schema(MOCK_TV_CONFIG_NO_TOKEN)(MOCK_TV_CONFIG_NO_TOKEN), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -659,7 +659,7 @@ async def test_import_flow_additional_configs( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS not in config_entry.options @@ -688,7 +688,7 @@ async def test_import_error( data=vol.Schema(VIZIO_SCHEMA)(fail_entry), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Ensure error gets logged vizio_log_list = [ @@ -719,7 +719,7 @@ async def test_import_ignore( data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_zeroconf_flow( @@ -735,7 +735,7 @@ async def test_zeroconf_flow( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Apply discovery updates to entry to mimic when user hits submit without changing @@ -752,7 +752,7 @@ async def test_zeroconf_flow( result["flow_id"], user_input=user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_NAME] == NAME @@ -781,7 +781,7 @@ async def test_zeroconf_flow_already_configured( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -808,7 +808,7 @@ async def test_zeroconf_flow_with_port_in_host( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -825,7 +825,7 @@ async def test_zeroconf_dupe_fail( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) @@ -834,7 +834,7 @@ async def test_zeroconf_dupe_fail( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -858,7 +858,7 @@ async def test_zeroconf_ignore( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_zeroconf_no_unique_id( @@ -873,7 +873,7 @@ async def test_zeroconf_no_unique_id( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -898,7 +898,7 @@ async def test_zeroconf_abort_when_ignored( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -927,7 +927,7 @@ async def test_zeroconf_flow_already_configured_hostname( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -952,7 +952,7 @@ async def test_import_flow_already_configured_hostname( ) # Flow should abort because device was updated - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" assert entry.data[CONF_HOST] == HOST diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py index 20f030bb99f..c45ab430c2e 100644 --- a/tests/components/vulcan/test_config_flow.py +++ b/tests/components/vulcan/test_config_flow.py @@ -36,7 +36,7 @@ async def test_show_form(hass): result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -56,7 +56,7 @@ async def test_config_flow_auth_success( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -70,7 +70,7 @@ async def test_config_flow_auth_success( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -93,7 +93,7 @@ async def test_config_flow_auth_success_with_multiple_students( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -102,7 +102,7 @@ async def test_config_flow_auth_success_with_multiple_students( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -115,7 +115,7 @@ async def test_config_flow_auth_success_with_multiple_students( {"student": "0"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_config_flow_reauth_success( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -154,7 +154,7 @@ async def test_config_flow_reauth_success( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -180,7 +180,7 @@ async def test_config_flow_reauth_without_matching_entries( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -189,7 +189,7 @@ async def test_config_flow_reauth_without_matching_entries( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_matching_entries" @@ -202,7 +202,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} with patch( @@ -215,7 +215,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_token"} @@ -229,7 +229,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "expired_token"} @@ -243,7 +243,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_pin"} @@ -257,7 +257,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_symbol"} @@ -271,7 +271,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -285,7 +285,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "unknown"} @@ -310,7 +310,7 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -319,7 +319,7 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student {"use_saved_credentials": False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -332,7 +332,7 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -353,7 +353,7 @@ async def test_multiple_config_entries_using_saved_credentials(mock_student, has const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -366,7 +366,7 @@ async def test_multiple_config_entries_using_saved_credentials(mock_student, has {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -387,7 +387,7 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -396,7 +396,7 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -409,7 +409,7 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h {"student": "0"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -438,7 +438,7 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -447,7 +447,7 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -460,7 +460,7 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -489,7 +489,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -498,7 +498,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -507,7 +507,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -520,7 +520,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h {"student": "0"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -545,7 +545,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -557,7 +557,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=UnauthorizedCertificateException, ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -566,7 +566,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_credentials"} @@ -593,7 +593,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -605,7 +605,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=ClientConnectionError, ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -614,7 +614,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] == {"base": "cannot_connect"} @@ -639,7 +639,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -651,7 +651,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=Exception, ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -660,7 +660,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} @@ -688,7 +688,7 @@ async def test_student_already_exists(mock_account, mock_keystore, mock_student, const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -697,7 +697,7 @@ async def test_student_already_exists(mock_account, mock_keystore, mock_student, {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "all_student_already_configured" @@ -713,7 +713,7 @@ async def test_config_flow_auth_invalid_token(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -722,7 +722,7 @@ async def test_config_flow_auth_invalid_token(mock_keystore, hass): {CONF_TOKEN: "3S20000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_token"} @@ -739,7 +739,7 @@ async def test_config_flow_auth_invalid_region(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -748,7 +748,7 @@ async def test_config_flow_auth_invalid_region(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_symbol"} @@ -765,7 +765,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -774,7 +774,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_pin"} @@ -791,7 +791,7 @@ async def test_config_flow_auth_expired_token(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -800,7 +800,7 @@ async def test_config_flow_auth_expired_token(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_token"} @@ -817,7 +817,7 @@ async def test_config_flow_auth_connection_error(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -826,7 +826,7 @@ async def test_config_flow_auth_connection_error(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "cannot_connect"} @@ -843,7 +843,7 @@ async def test_config_flow_auth_unknown_error(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -852,6 +852,6 @@ async def test_config_flow_auth_unknown_error(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 68f70878592..c9254f77aac 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -45,7 +45,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: flow.hass = hass result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index 514376d58a5..8027759738f 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -106,13 +106,13 @@ async def test_options_flow(hass: HomeAssistant, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index c4414cdbefd..c4b8144b74d 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -29,7 +29,7 @@ async def test_minimum_fields(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -38,7 +38,7 @@ async def test_minimum_fields(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -60,7 +60,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -76,7 +76,7 @@ async def test_options(hass): CONF_VEHICLE_TYPE: "taxi", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_AVOID_FERRIES: True, @@ -122,7 +122,7 @@ async def test_import(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { @@ -148,7 +148,7 @@ async def test_dupe(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -157,13 +157,13 @@ async def test_dupe(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -172,7 +172,7 @@ async def test_dupe(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @pytest.mark.usefixtures("invalidate_config_entry") @@ -181,12 +181,12 @@ async def test_invalid_config_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/wemo/test_config_flow.py b/tests/components/wemo/test_config_flow.py index 81040e44c9c..0ffcb9d7f5e 100644 --- a/tests/components/wemo/test_config_flow.py +++ b/tests/components/wemo/test_config_flow.py @@ -18,5 +18,5 @@ async def test_not_discovered(hass: HomeAssistant) -> None: with patch("homeassistant.components.wemo.config_flow.pywemo") as mock_pywemo: mock_pywemo.discover_devices.return_value = [] result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index 50433d377a9..655642dffc1 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -125,13 +125,13 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMEOUT: 9} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMEOUT] == 9 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b90a004ed0b..66e7d6b7055 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -199,7 +199,7 @@ class ComponentFactory: "redirect_uri": "http://127.0.0.1:8080/auth/external/callback", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={self._client_id}&" diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index f0530524805..9a6105a4b8b 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -38,7 +38,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -52,7 +52,7 @@ async def test_device_step_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device" @@ -71,7 +71,7 @@ async def test_create_entry(hass): {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result_create_entry["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result_create_entry["title"] == CONFIG[DEVICE_NAME] assert result_create_entry["data"] == CONFIG @@ -138,5 +138,5 @@ async def test_already_configured_error(hass): {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_create_entry["type"] == data_entry_flow.FlowResultType.ABORT assert result_create_entry["reason"] == "already_configured" diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index 4fe3554941d..da5a16882e8 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -126,7 +126,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -141,7 +141,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == { "1": "one", "2": "too", diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index f8c296dcbbe..2ce497bcbc1 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -19,7 +19,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( "xbox", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 2aa27d703a1..e47a1a1ace5 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -872,7 +872,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -882,7 +882,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_CLOUD_SUBDEVICES: True, } @@ -912,7 +912,7 @@ async def test_options_flow_incomplete(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -921,7 +921,7 @@ async def test_options_flow_incomplete(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cloud_credentials_incomplete"} diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index fc9a740d4a6..a3fb0cf6211 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -128,13 +128,13 @@ async def test_user_input_device_not_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "none"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -146,13 +146,13 @@ async def test_user_input_non_yamaha_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "no_musiccast_device"} @@ -176,7 +176,7 @@ async def test_user_input_device_already_existing( {"host": "192.168.188.18"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -188,13 +188,13 @@ async def test_user_input_unknown_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -209,13 +209,13 @@ async def test_user_input_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -235,13 +235,13 @@ async def test_user_input_device_found_no_ssdp( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -269,7 +269,7 @@ async def test_ssdp_discovery_failed(hass, mock_ssdp_no_yamaha, mock_get_source_ ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "yxc_control_url_missing" @@ -291,7 +291,7 @@ async def test_ssdp_discovery_successful_add_device( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "confirm" @@ -300,7 +300,7 @@ async def test_ssdp_discovery_successful_add_device( {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -332,7 +332,7 @@ async def test_ssdp_discovery_existing_device_update( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data[CONF_HOST] == "127.0.0.1" assert mock_entry.data["upnp_description"] == "http://127.0.0.1/desc.xml" diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index e224bc3e1d2..f809596e816 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -22,7 +22,7 @@ async def test_abort_if_no_configuration(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -32,7 +32,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -60,7 +60,7 @@ async def test_full_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -126,7 +126,7 @@ async def test_abort_if_authorization_timeout(hass, current_request_with_host): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -203,6 +203,6 @@ async def test_reauthentication( assert token_data["refresh_token"] == "mock-refresh-token" assert token_data["type"] == "Bearer" assert token_data["expires_in"] == 60 - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 979aa8bf088..dcb0ee7abfd 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -51,7 +51,7 @@ async def test_single_entry_allowed(hass, discovery_flow_conf): MockConfigEntry(domain="test").add_to_hass(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -62,7 +62,7 @@ async def test_user_no_devices_found(hass, discovery_flow_conf): flow.context = {"source": config_entries.SOURCE_USER} result = await flow.async_step_confirm(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -75,7 +75,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): "test", context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" progress = hass.config_entries.flow.async_progress() @@ -88,7 +88,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): } result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -110,7 +110,7 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): MockConfigEntry(domain="test").add_to_hass(hass) result = await getattr(flow, f"async_step_{source}")({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -132,11 +132,11 @@ async def test_discovery_confirmation(hass, discovery_flow_conf, source): result = await getattr(flow, f"async_step_{source}")({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await flow.async_step_confirm({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -160,7 +160,7 @@ async def test_discovery_during_onboarding(hass, discovery_flow_conf, source): ): result = await getattr(flow, f"async_step_{source}")({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_multiple_discoveries(hass, discovery_flow_conf): @@ -170,13 +170,13 @@ async def test_multiple_discoveries(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Second discovery result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_only_one_in_progress(hass, discovery_flow_conf): @@ -187,21 +187,21 @@ async def test_only_one_in_progress(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # User starts flow result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Discovery flow has not been aborted assert len(hass.config_entries.flow.async_progress()) == 2 # Discovery should be aborted once user confirms result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -213,14 +213,14 @@ async def test_import_abort_discovery(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Start import flow result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_IMPORT}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Discovery flow has been aborted assert len(hass.config_entries.flow.async_progress()) == 0 @@ -234,7 +234,7 @@ async def test_import_no_confirmation(hass, discovery_flow_conf): discovery_flow_conf["discovered"] = True result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_import_single_instance(hass, discovery_flow_conf): @@ -246,7 +246,7 @@ async def test_import_single_instance(hass, discovery_flow_conf): MockConfigEntry(domain="test").add_to_hass(hass) result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_ignored_discoveries(hass, discovery_flow_conf): @@ -256,7 +256,7 @@ async def test_ignored_discoveries(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM flow = next( ( @@ -278,7 +278,7 @@ async def test_ignored_discoveries(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_webhook_single_entry_allowed(hass, webhook_flow_conf): @@ -289,7 +289,7 @@ async def test_webhook_single_entry_allowed(hass, webhook_flow_conf): MockConfigEntry(domain="test_single").add_to_hass(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -302,7 +302,7 @@ async def test_webhook_multiple_entries_allowed(hass, webhook_flow_conf): hass.config.api = Mock(base_url="http://example.com") result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): @@ -316,7 +316,7 @@ async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): ) result = await flow.async_step_user(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["webhook_id"] is not None @@ -341,7 +341,7 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): result = await hass.config_entries.flow.async_init( "test_single", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "hass_nabucasa.cloudhooks.Cloudhooks.async_create", @@ -358,7 +358,7 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["description_placeholders"]["webhook_url"] == "https://example.com" assert len(mock_create.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 @@ -395,7 +395,7 @@ async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_ result = await hass.config_entries.flow.async_init( "test_single", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "hass_nabucasa.cloudhooks.Cloudhooks.async_create", @@ -413,7 +413,7 @@ async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_ result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index e5d220c55df..652ce69e57d 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -110,7 +110,7 @@ async def test_abort_if_no_implementation(hass, flow_handler): flow = flow_handler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -121,7 +121,7 @@ async def test_missing_credentials_for_domain(hass, flow_handler): with patch("homeassistant.loader.APPLICATION_CREDENTIALS", [TEST_DOMAIN]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -139,7 +139,7 @@ async def test_abort_if_authorization_timeout( ): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -157,7 +157,7 @@ async def test_abort_if_no_url_available( ): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_url_available" @@ -179,7 +179,7 @@ async def test_abort_if_oauth_error( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # Pick implementation @@ -195,7 +195,7 @@ async def test_abort_if_oauth_error( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -219,7 +219,7 @@ async def test_abort_if_oauth_error( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "oauth_error" @@ -241,7 +241,7 @@ async def test_abort_if_oauth_rejected( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # Pick implementation @@ -257,7 +257,7 @@ async def test_abort_if_oauth_rejected( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -273,7 +273,7 @@ async def test_abort_if_oauth_rejected( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "user_rejected_authorize" assert result["description_placeholders"] == {"error": "access_denied"} @@ -291,7 +291,7 @@ async def test_step_discovery(hass, flow_handler, local_impl): data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" @@ -308,7 +308,7 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" result = await hass.config_entries.flow.async_init( @@ -317,7 +317,7 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -340,7 +340,7 @@ async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl) data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -362,7 +362,7 @@ async def test_full_flow( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # Pick implementation @@ -378,7 +378,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" diff --git a/tests/helpers/test_helper_config_entry_flow.py b/tests/helpers/test_helper_config_entry_flow.py index 46e8998c738..2967b202efe 100644 --- a/tests/helpers/test_helper_config_entry_flow.py +++ b/tests/helpers/test_helper_config_entry_flow.py @@ -38,7 +38,7 @@ def manager(): async def async_finish_flow(self, flow, result): """Test finish flow.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: result["source"] = flow.context.get("source") entries.append(result) return result @@ -110,11 +110,11 @@ async def test_config_flow_advanced_option( # Start flow in basic mode result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == ["option1"] result = await manager.async_configure(result["flow_id"], {"option1": "blabla"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert result["options"] == { "advanced_default": "a very reasonable default", @@ -126,7 +126,7 @@ async def test_config_flow_advanced_option( # Start flow in advanced mode result = await manager.async_init("test", context={"show_advanced_options": True}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -136,7 +136,7 @@ async def test_config_flow_advanced_option( result = await manager.async_configure( result["flow_id"], {"advanced_no_default": "abc123", "option1": "blabla"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert result["options"] == { "advanced_default": "a very reasonable default", @@ -149,7 +149,7 @@ async def test_config_flow_advanced_option( # Start flow in advanced mode result = await manager.async_init("test", context={"show_advanced_options": True}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -164,7 +164,7 @@ async def test_config_flow_advanced_option( "option1": "blabla", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert result["options"] == { "advanced_default": "not default", @@ -216,13 +216,13 @@ async def test_options_flow_advanced_option( # Start flow in basic mode result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == ["option1"] result = await hass.config_entries.options.async_configure( result["flow_id"], {"option1": "blublu"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "advanced_default": "not default", "advanced_no_default": "abc123", @@ -236,7 +236,7 @@ async def test_options_flow_advanced_option( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -246,7 +246,7 @@ async def test_options_flow_advanced_option( result = await hass.config_entries.options.async_configure( result["flow_id"], {"advanced_no_default": "def456", "option1": "blabla"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "advanced_default": "a very reasonable default", "advanced_no_default": "def456", @@ -260,7 +260,7 @@ async def test_options_flow_advanced_option( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -275,7 +275,7 @@ async def test_options_flow_advanced_option( "option1": "blabla", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "advanced_default": "also not default", "advanced_no_default": "abc123", diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 9372a906f71..03da4d36853 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -713,14 +713,14 @@ async def test_discovery_notification(hass): ) flow1 = await hass.config_entries.flow.async_configure(flow1["flow_id"], {}) - assert flow1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_discovery") assert state is not None flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) - assert flow2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_discovery") @@ -780,14 +780,14 @@ async def test_reauth_notification(hass): ) flow1 = await hass.config_entries.flow.async_configure(flow1["flow_id"], {}) - assert flow1["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow1["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_reconfigure") assert state is not None flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) - assert flow2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow2["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_reconfigure") @@ -1059,7 +1059,7 @@ async def test_entry_options(hass, manager): await manager.options.async_finish_flow( flow, - {"data": {"second": True}, "type": data_entry_flow.RESULT_TYPE_CREATE_ENTRY}, + {"data": {"second": True}, "type": data_entry_flow.FlowResultType.CREATE_ENTRY}, ) assert entry.data == {"first": True} @@ -1092,7 +1092,7 @@ async def test_entry_options_abort(hass, manager): flow.handler = entry.entry_id # Used to keep reference to config entry assert await manager.options.async_finish_flow( - flow, {"type": data_entry_flow.RESULT_TYPE_ABORT, "reason": "test"} + flow, {"type": data_entry_flow.FlowResultType.ABORT, "reason": "test"} ) @@ -1574,7 +1574,7 @@ async def test_unique_id_existing_entry(hass, manager): "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries("comp") assert len(entries) == 1 @@ -1860,14 +1860,14 @@ async def test_unique_id_in_progress(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Will be canceled result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -1898,14 +1898,14 @@ async def test_finish_flow_aborts_progress(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Will finish and cancel other one. result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER}, data={} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -1931,7 +1931,7 @@ async def test_unique_id_ignore(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await manager.flow.async_init( "comp", @@ -1939,7 +1939,7 @@ async def test_unique_id_ignore(hass, manager): data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # assert len(hass.config_entries.flow.async_progress()) == 0 @@ -1992,7 +1992,7 @@ async def test_manual_add_overrides_ignored_entry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" assert len(async_reload.mock_calls) == 0 @@ -2169,7 +2169,7 @@ async def test_unignore_step_form(hass, manager): context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries("comp")[0] assert entry.source == "ignore" @@ -2214,7 +2214,7 @@ async def test_unignore_create_entry(hass, manager): context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries("comp")[0] assert entry.source == "ignore" @@ -2256,7 +2256,7 @@ async def test_unignore_default_impl(hass, manager): context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries("comp")[0] assert entry.source == "ignore" @@ -2321,7 +2321,7 @@ async def test_partial_flows_hidden(hass, manager): # When it's complete it should now be visible in async_progress and have triggered # discovery notifications result = await init_task - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert len(hass.config_entries.flow.async_progress()) == 1 await hass.async_block_till_done() @@ -2531,7 +2531,7 @@ async def test_flow_with_default_discovery(hass, manager, discovery_source): result = await manager.flow.async_init( "comp", context={"source": discovery_source[0]}, data=discovery_source[1] ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2544,7 +2544,7 @@ async def test_flow_with_default_discovery(hass, manager, discovery_source): result2 = await manager.flow.async_configure( result["flow_id"], user_input={"fake": "data"} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -2575,7 +2575,7 @@ async def test_flow_with_default_discovery_with_unique_id(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2600,7 +2600,7 @@ async def test_default_discovery_abort_existing_entries(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -2626,13 +2626,13 @@ async def test_default_discovery_in_progress(hass, manager): context={"source": config_entries.SOURCE_DISCOVERY}, data={"unique_id": "mock-unique-id"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Second discovery without a unique ID result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2660,7 +2660,7 @@ async def test_default_discovery_abort_on_new_unique_flow(hass, manager): result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM # Second discovery brings in a unique ID result = await manager.flow.async_init( @@ -2668,7 +2668,7 @@ async def test_default_discovery_abort_on_new_unique_flow(hass, manager): context={"source": config_entries.SOURCE_DISCOVERY}, data={"unique_id": "mock-unique-id"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Ensure the first one is cancelled and we end up with just the last one flows = hass.config_entries.flow.async_progress() @@ -2702,7 +2702,7 @@ async def test_default_discovery_abort_on_user_flow_complete(hass, manager): flow1 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert flow1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow1["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2711,14 +2711,14 @@ async def test_default_discovery_abort_on_user_flow_complete(hass, manager): flow2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert flow2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow2["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 2 # Complete the manual flow result = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Ensure the first flow is gone now flows = hass.config_entries.flow.async_progress() @@ -2780,7 +2780,7 @@ async def test_flow_same_device_multiple_sources(hass, manager): result2 = await manager.flow.async_configure( flows[0]["flow_id"], user_input={"fake": "data"} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 18d5469a162..301a61700c9 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -34,7 +34,7 @@ def manager(): async def async_finish_flow(self, flow, result): """Test finish flow.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: result["source"] = flow.context.get("source") entries.append(result) return result @@ -100,7 +100,7 @@ async def test_configure_two_steps(manager): form = await manager.async_configure(form["flow_id"], ["INIT-DATA"]) form = await manager.async_configure(form["flow_id"], ["SECOND-DATA"]) - assert form["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert form["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(manager.async_progress()) == 0 assert len(manager.mock_created_entries) == 1 result = manager.mock_created_entries[0] @@ -122,7 +122,7 @@ async def test_show_form(manager): ) form = await manager.async_init("test") - assert form["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form["type"] == data_entry_flow.FlowResultType.FORM assert form["data_schema"] is schema assert form["errors"] == {"username": "Should be unique."} @@ -218,7 +218,7 @@ async def test_finish_callback_change_result_type(hass): async def async_finish_flow(self, flow, result): """Redirect to init form if count <= 1.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: if result["data"] is None or result["data"].get("count", 0) <= 1: return flow.async_show_form( step_id="init", data_schema=vol.Schema({"count": int}) @@ -230,16 +230,16 @@ async def test_finish_callback_change_result_type(hass): manager = FlowManager(hass) result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await manager.async_configure(result["flow_id"], {"count": 0}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert "result" not in result result = await manager.async_configure(result["flow_id"], {"count": 2}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"] == 2 @@ -269,7 +269,7 @@ async def test_external_step(hass, manager): ) result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert len(manager.async_progress()) == 1 assert len(manager.async_progress_by_handler("test")) == 1 assert manager.async_get(result["flow_id"])["handler"] == "test" @@ -277,7 +277,7 @@ async def test_external_step(hass, manager): # Mimic external step # Called by integrations: `hass.config_entries.flow.async_configure(…)` result = await manager.async_configure(result["flow_id"], {"title": "Hello"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP_DONE await hass.async_block_till_done() assert len(events) == 1 @@ -289,7 +289,7 @@ async def test_external_step(hass, manager): # Frontend refreshses the flow result = await manager.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Hello" @@ -326,7 +326,7 @@ async def test_show_progress(hass, manager): ) result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_one" assert len(manager.async_progress()) == 1 assert len(manager.async_progress_by_handler("test")) == 1 @@ -335,7 +335,7 @@ async def test_show_progress(hass, manager): # Mimic task one done and moving to task two # Called by integrations: `hass.config_entries.flow.async_configure(…)` result = await manager.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_two" await hass.async_block_till_done() @@ -349,7 +349,7 @@ async def test_show_progress(hass, manager): # Mimic task two done and continuing step # Called by integrations: `hass.config_entries.flow.async_configure(…)` result = await manager.async_configure(result["flow_id"], {"title": "Hello"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE await hass.async_block_till_done() assert len(events) == 2 @@ -361,7 +361,7 @@ async def test_show_progress(hass, manager): # Frontend refreshes the flow result = await manager.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Hello" @@ -374,7 +374,7 @@ async def test_abort_flow_exception(manager): raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"}) form = await manager.async_init("test") - assert form["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert form["type"] == data_entry_flow.FlowResultType.ABORT assert form["reason"] == "mock-reason" assert form["description_placeholders"] == {"placeholder": "yo"} @@ -432,7 +432,7 @@ async def test_async_has_matching_flow( context={"source": config_entries.SOURCE_HOMEKIT}, data={"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_one" assert len(manager.async_progress()) == 1 assert len(manager.async_progress_by_handler("test")) == 1 @@ -528,5 +528,5 @@ async def test_show_menu(hass, manager, menu_options): result = await manager.async_configure( result["flow_id"], {"next_step_id": "target1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "target1" From d3db4da11a668d64867289f6b5999f1eb0288d6e Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 7 Jul 2022 13:07:05 -0400 Subject: [PATCH 194/820] ElkM1 bump lib to support Python 3.10 SSL (#74569) Co-authored-by: J. Nick Koston --- homeassistant/components/elkm1/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 13f8ba8401f..7043fdfcb9d 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,7 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==2.0.0"], + "requirements": ["elkm1-lib==2.0.2"], "dhcp": [{ "registered_devices": true }, { "macaddress": "00409D*" }], "codeowners": ["@gwww", "@bdraco"], "dependencies": ["network"], diff --git a/requirements_all.txt b/requirements_all.txt index 838e1278d75..7df6a75a946 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -586,7 +586,7 @@ elgato==3.0.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==2.0.0 +elkm1-lib==2.0.2 # homeassistant.components.elmax elmax_api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9c98ba9fb2..02b5ef640d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ eagle100==0.1.1 elgato==3.0.0 # homeassistant.components.elkm1 -elkm1-lib==2.0.0 +elkm1-lib==2.0.2 # homeassistant.components.elmax elmax_api==0.0.2 From ff877b8144a8c08eb82810dc997fb95987bbe43a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jul 2022 12:46:19 -0500 Subject: [PATCH 195/820] Fix exception in doorbird logbook during startup (#74649) * Fix exception in doorbird logbook during startup Fixes ``` 2022-07-07 16:50:33.203 ERROR (MainThread) [homeassistant.helpers.integration_platform] Error processing platform doorbird.logbook Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/helpers/integration_platform.py", line 51, in _async_process_single_integration_platform_component await integration_platform.process_platform(hass, component_name, platform) File "/usr/src/homeassistant/homeassistant/components/logbook/__init__.py", line 159, in _process_logbook_platform platform.async_describe_events(hass, _async_describe_event) File "/usr/src/homeassistant/homeassistant/components/doorbird/logbook.py", line 43, in async_describe_events door_station = data[DOOR_STATION] KeyError: door_station ``` * py39 --- homeassistant/components/doorbird/logbook.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index 110d3f22fbf..3b1563c2880 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -1,4 +1,7 @@ """Describe logbook events.""" +from __future__ import annotations + +from typing import Any from homeassistant.components.logbook.const import ( LOGBOOK_ENTRY_ENTITY_ID, @@ -28,12 +31,13 @@ def async_describe_events(hass, async_describe_event): ].get(doorbird_event, event.data.get(ATTR_ENTITY_ID)), } - domain_data = hass.data[DOMAIN] + domain_data: dict[str, Any] = hass.data[DOMAIN] - for config_entry_id in domain_data: - door_station = domain_data[config_entry_id][DOOR_STATION] - - for event in door_station.doorstation_events: + for data in domain_data.values(): + if DOOR_STATION not in data: + # We need to skip door_station_event_entity_ids + continue + for event in data[DOOR_STATION].doorstation_events: async_describe_event( DOMAIN, f"{DOMAIN}_{event}", async_describe_logbook_event ) From f2706aa8c0eb3edb6ffeee01c77977800d95749e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 19:48:39 +0200 Subject: [PATCH 196/820] Update debugpy to 1.6.1 (#74637) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 00e4839be63..3f31c5de758 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.6.0"], + "requirements": ["debugpy==1.6.1"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 7df6a75a946..7cc20779883 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.0 +debugpy==1.6.1 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02b5ef640d7..870f830ac31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -388,7 +388,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.0 +debugpy==1.6.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From dc0c41982fe0626c487057cc63bcf4764f7fd5e4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 19:50:08 +0200 Subject: [PATCH 197/820] Remove meteo_france from mypy ignore list (#74613) --- homeassistant/components/meteo_france/const.py | 2 +- homeassistant/components/meteo_france/sensor.py | 9 ++++++++- homeassistant/components/meteo_france/weather.py | 5 +++++ mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index df667b1964b..d6cbd34d88d 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -183,7 +183,7 @@ SENSOR_TYPES_PROBABILITY: tuple[MeteoFranceSensorEntityDescription, ...] = ( ) -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire"], ATTR_CONDITION_CLOUDY: ["Très nuageux", "Couvert"], ATTR_CONDITION_FOG: [ diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 0d90e4d33be..823018f405f 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,4 +1,6 @@ """Support for Meteo-France raining forecast sensor.""" +from __future__ import annotations + from meteofrance_api.helpers import ( get_warning_text_status_from_indice_color, readeable_phenomenoms_dict, @@ -97,6 +99,11 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" + assert ( + self.platform + and self.platform.config_entry + and self.platform.config_entry.unique_id + ) return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, @@ -191,7 +198,7 @@ class MeteoFranceAlertSensor(MeteoFranceSensor): def _find_first_probability_forecast_not_null( probability_forecast: list, path: list -) -> int: +) -> int | None: """Search the first not None value in the first forecast elements.""" for forecast in probability_forecast[0:3]: if forecast[path[1]][path[2]] is not None: diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index a30a65304b0..2727b4470c1 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -102,6 +102,11 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" + assert ( + self.platform + and self.platform.config_entry + and self.platform.config_entry.unique_id + ) return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, diff --git a/mypy.ini b/mypy.ini index fc99119cda3..1f0018a4c05 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2701,12 +2701,6 @@ ignore_errors = true [mypy-homeassistant.components.lovelace.websocket] ignore_errors = true -[mypy-homeassistant.components.meteo_france.sensor] -ignore_errors = true - -[mypy-homeassistant.components.meteo_france.weather] -ignore_errors = true - [mypy-homeassistant.components.minecraft_server] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 7f1faf00bb6..3aedaced494 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -42,8 +42,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", "homeassistant.components.lovelace.websocket", - "homeassistant.components.meteo_france.sensor", - "homeassistant.components.meteo_france.weather", "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", From bd43f0393cc5f031bda02d232b6bd8906d8b81e1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 19:51:38 +0200 Subject: [PATCH 198/820] Remove influxdb from mypy ignore list (#74612) --- homeassistant/components/influxdb/__init__.py | 28 ++++++++++++------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index ecc76481e49..72871c75fc4 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -30,7 +30,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import event as event_helper, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues @@ -198,13 +198,13 @@ CONFIG_SCHEMA = vol.Schema( ) -def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: +def _generate_event_to_json(conf: dict) -> Callable[[Event], dict[str, Any] | None]: """Build event to json converter and add to config.""" entity_filter = convert_include_exclude_filter(conf) tags = conf.get(CONF_TAGS) - tags_attributes = conf.get(CONF_TAGS_ATTRIBUTES) + tags_attributes: list[str] = conf[CONF_TAGS_ATTRIBUTES] default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT) - measurement_attr = conf.get(CONF_MEASUREMENT_ATTR) + measurement_attr: str = conf[CONF_MEASUREMENT_ATTR] override_measurement = conf.get(CONF_OVERRIDE_MEASUREMENT) global_ignore_attributes = set(conf[CONF_IGNORE_ATTRIBUTES]) component_config = EntityValues( @@ -213,15 +213,15 @@ def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: conf[CONF_COMPONENT_CONFIG_GLOB], ) - def event_to_json(event: dict) -> str: + def event_to_json(event: Event) -> dict[str, Any] | None: """Convert event into json in format Influx expects.""" - state = event.data.get(EVENT_NEW_STATE) + state: State | None = event.data.get(EVENT_NEW_STATE) if ( state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) or not entity_filter(state.entity_id) ): - return + return None try: _include_state = _include_value = False @@ -263,7 +263,7 @@ def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: else: include_uom = measurement_attr != "unit_of_measurement" - json = { + json: dict[str, Any] = { INFLUX_CONF_MEASUREMENT: measurement, INFLUX_CONF_TAGS: { CONF_DOMAIN: state.domain, @@ -328,7 +328,9 @@ class InfluxClient: close: Callable[[], None] -def get_influx_connection(conf, test_write=False, test_read=False): # noqa: C901 +def get_influx_connection( # noqa: C901 + conf, test_write=False, test_read=False +) -> InfluxClient: """Create the correct influx connection for the API version.""" kwargs = { CONF_TIMEOUT: TIMEOUT, @@ -470,6 +472,10 @@ def get_influx_connection(conf, test_write=False, test_read=False): # noqa: C90 return InfluxClient(databases, write_v1, query_v1, close_v1) +def _retry_setup(hass: HomeAssistant, config: ConfigType) -> None: + setup(hass, config) + + def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the InfluxDB component.""" conf = config[DOMAIN] @@ -477,7 +483,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: influx = get_influx_connection(conf, test_write=True) except ConnectionError as exc: _LOGGER.error(RETRY_MESSAGE, exc) - event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) + event_helper.call_later( + hass, RETRY_INTERVAL, lambda _: _retry_setup(hass, config) + ) return True event_to_json = _generate_event_to_json(conf) diff --git a/mypy.ini b/mypy.ini index 1f0018a4c05..b139a40976b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2665,9 +2665,6 @@ ignore_errors = true [mypy-homeassistant.components.icloud.sensor] ignore_errors = true -[mypy-homeassistant.components.influxdb] -ignore_errors = true - [mypy-homeassistant.components.izone.climate] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3aedaced494..55ed01aaa63 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -30,7 +30,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", "homeassistant.components.icloud.sensor", - "homeassistant.components.influxdb", "homeassistant.components.izone.climate", "homeassistant.components.konnected", "homeassistant.components.konnected.config_flow", From ac85a3ce64ea815fd3530085c085e384cf8269fb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 7 Jul 2022 20:13:03 +0200 Subject: [PATCH 199/820] Use pydeconz interface controls for button platform (#74654) --- homeassistant/components/deconz/button.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/button.py b/homeassistant/components/deconz/button.py index 498c88a2351..552723d6f9c 100644 --- a/homeassistant/components/deconz/button.py +++ b/homeassistant/components/deconz/button.py @@ -90,8 +90,11 @@ class DeconzButton(DeconzSceneMixin, ButtonEntity): async def async_press(self) -> None: """Store light states into scene.""" - async_button_fn = getattr(self._device, self.entity_description.button_fn) - await async_button_fn() + async_button_fn = getattr( + self.gateway.api.scenes, + self.entity_description.button_fn, + ) + await async_button_fn(self._device.group_id, self._device.id) def get_device_identifier(self) -> str: """Return a unique identifier for this scene.""" From b9b6ed33ee5c0d5ade23790c7c37f3a382269fcd Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 7 Jul 2022 20:27:48 +0200 Subject: [PATCH 200/820] Fix smart energy polling for Tuya plugs (#74640) * Add PolledSmartEnergySummation to poll summation_delivered for some ZHA plugs * Remove PolledSmartEnergyMetering, add stop_on_match_group to summation sensors --- homeassistant/components/zha/sensor.py | 37 ++++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 4a4700b3c4c..d9aa44fe436 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -472,25 +472,8 @@ class SmartEnergyMetering(Sensor): @MULTI_MATCH( channel_names=CHANNEL_SMARTENERGY_METERING, - models={"TS011F"}, stop_on_match_group=CHANNEL_SMARTENERGY_METERING, ) -class PolledSmartEnergyMetering(SmartEnergyMetering): - """Polled metering sensor.""" - - @property - def should_poll(self) -> bool: - """Poll the entity for current state.""" - return True - - async def async_update(self) -> None: - """Retrieve latest state.""" - if not self.available: - return - await self._channel.async_force_update() - - -@MULTI_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered"): """Smart Energy Metering summation sensor.""" @@ -523,6 +506,26 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered") return round(cooked, 3) +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"TS011F"}, + stop_on_match_group=CHANNEL_SMARTENERGY_METERING, +) +class PolledSmartEnergySummation(SmartEnergySummation): + """Polled Smart Energy Metering summation sensor.""" + + @property + def should_poll(self) -> bool: + """Poll the entity for current state.""" + return True + + async def async_update(self) -> None: + """Retrieve latest state.""" + if not self.available: + return + await self._channel.async_force_update() + + @MULTI_MATCH(channel_names=CHANNEL_PRESSURE) class Pressure(Sensor): """Pressure sensor.""" From d8030ed9e7c92279316240e6d71095345883ebb6 Mon Sep 17 00:00:00 2001 From: Arne Mauer Date: Thu, 7 Jul 2022 20:28:33 +0200 Subject: [PATCH 201/820] Ikea Starkvind support all models (#74615) * Add Particulate Matter 2.5 of ZCL concentration clusters to ZHA component * Fixed black and flake8 test * New sensors and manufacturer cluster to support IKEA STARKVIND (with quirk) * Fix multi_match for FilterLifeTime, device_run_time, filter_run_time sensors for Ikea starkvind * Remove model match because sensors are matched with manufacturer channel * Update manufacturerspecific.py * Update number.py --- homeassistant/components/zha/binary_sensor.py | 2 +- homeassistant/components/zha/number.py | 4 +--- homeassistant/components/zha/sensor.py | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 709515d7ca2..23d26e4b13f 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -188,7 +188,7 @@ class FrostLock(BinarySensor, id_suffix="frost_lock"): _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.LOCK -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH(channel_names="ikea_airpurifier") class ReplaceFilter(BinarySensor, id_suffix="replace_filter"): """ZHA BinarySensor.""" diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index e1268e29190..36fc5267bd9 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -525,9 +525,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati _zcl_attribute: str = "timer_duration" -@CONFIG_DIAGNOSTIC_MATCH( - channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} -) +@CONFIG_DIAGNOSTIC_MATCH(channel_names="ikea_airpurifier") class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): """Representation of a ZHA timer duration configuration entity.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index d9aa44fe436..513ba5510b5 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -813,7 +813,7 @@ class TimeLeft(Sensor, id_suffix="time_left"): _unit = TIME_MINUTES -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH(channel_names="ikea_airpurifier") class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): """Sensor that displays device run time (in minutes).""" @@ -823,7 +823,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): _unit = TIME_MINUTES -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH(channel_names="ikea_airpurifier") class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): """Sensor that displays run time of the current filter (in minutes).""" From 3f53ed5d5cddfebd302a2d4fad45d271943533d6 Mon Sep 17 00:00:00 2001 From: Kostas Chatzikokolakis Date: Thu, 7 Jul 2022 21:31:03 +0300 Subject: [PATCH 202/820] Add trigger/arm_custom_bypass to Template Alarm Control Panel (#74629) --- .../template/alarm_control_panel.py | 40 +++++++++++++++++++ .../template/test_alarm_control_panel.py | 16 ++++++++ 2 files changed, 56 insertions(+) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 132e9fb0ca5..d7c117c9be7 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_VACATION, @@ -42,6 +43,7 @@ from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_con _LOGGER = logging.getLogger(__name__) _VALID_STATES = [ STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_VACATION, @@ -53,10 +55,12 @@ _VALID_STATES = [ ] CONF_ARM_AWAY_ACTION = "arm_away" +CONF_ARM_CUSTOM_BYPASS_ACTION = "arm_custom_bypass" CONF_ARM_HOME_ACTION = "arm_home" CONF_ARM_NIGHT_ACTION = "arm_night" CONF_ARM_VACATION_ACTION = "arm_vacation" CONF_DISARM_ACTION = "disarm" +CONF_TRIGGER_ACTION = "trigger" CONF_ALARM_CONTROL_PANELS = "panels" CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_FORMAT = "code_format" @@ -75,9 +79,11 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum( TemplateCodeFormat @@ -164,6 +170,16 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): self._arm_vacation_script = None if (arm_vacation_action := config.get(CONF_ARM_VACATION_ACTION)) is not None: self._arm_vacation_script = Script(hass, arm_vacation_action, name, DOMAIN) + self._arm_custom_bypass_script = None + if ( + arm_custom_bypass_action := config.get(CONF_ARM_CUSTOM_BYPASS_ACTION) + ) is not None: + self._arm_custom_bypass_script = Script( + hass, arm_custom_bypass_action, name, DOMAIN + ) + self._trigger_script = None + if (trigger_action := config.get(CONF_TRIGGER_ACTION)) is not None: + self._trigger_script = Script(hass, trigger_action, name, DOMAIN) self._state: str | None = None @@ -196,6 +212,16 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): supported_features | AlarmControlPanelEntityFeature.ARM_VACATION ) + if self._arm_custom_bypass_script is not None: + supported_features = ( + supported_features | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + ) + + if self._trigger_script is not None: + supported_features = ( + supported_features | AlarmControlPanelEntityFeature.TRIGGER + ) + return supported_features @property @@ -275,8 +301,22 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): STATE_ALARM_ARMED_VACATION, script=self._arm_vacation_script, code=code ) + async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: + """Arm the panel to Custom Bypass.""" + await self._async_alarm_arm( + STATE_ALARM_ARMED_CUSTOM_BYPASS, + script=self._arm_custom_bypass_script, + code=code, + ) + async def async_alarm_disarm(self, code: str | None = None) -> None: """Disarm the panel.""" await self._async_alarm_arm( STATE_ALARM_DISARMED, script=self._disarm_script, code=code ) + + async def async_alarm_trigger(self, code: str | None = None) -> None: + """Trigger the panel.""" + await self._async_alarm_arm( + STATE_ALARM_TRIGGERED, script=self._trigger_script, code=code + ) diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 9e51b48dcc6..7ae69b2d563 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -8,6 +8,7 @@ from homeassistant.const import ( ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_VACATION, @@ -62,11 +63,21 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = { "entity_id": "alarm_control_panel.test", "data": {"code": "{{ this.entity_id }}"}, }, + "arm_custom_bypass": { + "service": "alarm_control_panel.alarm_arm_custom_bypass", + "entity_id": "alarm_control_panel.test", + "data": {"code": "{{ this.entity_id }}"}, + }, "disarm": { "service": "alarm_control_panel.alarm_disarm", "entity_id": "alarm_control_panel.test", "data": {"code": "{{ this.entity_id }}"}, }, + "trigger": { + "service": "alarm_control_panel.alarm_trigger", + "entity_id": "alarm_control_panel.test", + "data": {"code": "{{ this.entity_id }}"}, + }, } @@ -96,6 +107,7 @@ async def test_template_state_text(hass, start_ha): STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_VACATION, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -136,7 +148,9 @@ async def test_optimistic_states(hass, start_ha): ("alarm_arm_home", STATE_ALARM_ARMED_HOME), ("alarm_arm_night", STATE_ALARM_ARMED_NIGHT), ("alarm_arm_vacation", STATE_ALARM_ARMED_VACATION), + ("alarm_arm_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS), ("alarm_disarm", STATE_ALARM_DISARMED), + ("alarm_trigger", STATE_ALARM_TRIGGERED), ]: await hass.services.async_call( ALARM_DOMAIN, service, {"entity_id": TEMPLATE_NAME}, blocking=True @@ -259,7 +273,9 @@ async def test_name(hass, start_ha): "alarm_arm_away", "alarm_arm_night", "alarm_arm_vacation", + "alarm_arm_custom_bypass", "alarm_disarm", + "alarm_trigger", ], ) async def test_actions(hass, service, start_ha, service_calls): From 46beae9061d33d3c88454d0bedb4984cf934e033 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 20:55:47 +0200 Subject: [PATCH 203/820] Bump number of test groups from 6 -> 10 (#74648) --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6a4413e3668..2f97a8e4208 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,8 +93,8 @@ jobs: # Defaults integrations_glob="" test_full_suite="true" - test_groups="[1, 2, 3, 4, 5, 6]" - test_group_count=6 + test_groups="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" + test_group_count=10 tests="[]" tests_glob="" @@ -137,8 +137,8 @@ jobs: || [[ "${{ github.event.inputs.full }}" == "true" ]] \ || [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-full-run') }}" == "true" ]]; then - test_groups="[1, 2, 3, 4, 5, 6]" - test_group_count=6 + test_groups="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" + test_group_count=10 test_full_suite="true" fi From a6244eea28bc44e3c5f323f9280ea607fa7bd744 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 21:28:18 +0200 Subject: [PATCH 204/820] Search/replace RESULT_TYPE_* by FlowResultType enum (#74656) --- tests/auth/providers/test_trusted_networks.py | 14 +-- tests/components/adax/test_config_flow.py | 44 +++++----- .../components/airthings/test_config_flow.py | 12 +-- .../aladdin_connect/test_config_flow.py | 28 +++--- tests/components/ambee/test_config_flow.py | 30 +++---- tests/components/anthemav/test_config_flow.py | 12 +-- .../aseko_pool_live/test_config_flow.py | 8 +- .../aussie_broadband/test_config_flow.py | 22 ++--- tests/components/baf/test_config_flow.py | 22 ++--- tests/components/balboa/test_config_flow.py | 18 ++-- tests/components/canary/test_config_flow.py | 22 ++--- .../components/cloudflare/test_config_flow.py | 28 +++--- .../components/co2signal/test_config_flow.py | 24 ++--- tests/components/cpuspeed/test_config_flow.py | 16 ++-- tests/components/daikin/test_config_flow.py | 22 ++--- tests/components/deluge/test_config_flow.py | 18 ++-- .../components/derivative/test_config_flow.py | 10 +-- .../devolo_home_network/test_config_flow.py | 20 ++--- tests/components/directv/test_config_flow.py | 36 ++++---- tests/components/dnsip/test_config_flow.py | 20 ++--- tests/components/econet/test_config_flow.py | 22 ++--- tests/components/efergy/test_config_flow.py | 20 ++--- .../eight_sleep/test_config_flow.py | 18 ++-- tests/components/elgato/test_config_flow.py | 26 +++--- tests/components/elkm1/test_config_flow.py | 34 +++---- tests/components/esphome/test_config_flow.py | 60 ++++++------- .../evil_genius_labs/test_config_flow.py | 12 +-- tests/components/ezviz/test_config_flow.py | 64 +++++++------- tests/components/filesize/test_config_flow.py | 16 ++-- tests/components/fivem/test_config_flow.py | 14 +-- .../fjaraskupan/test_config_flow.py | 14 ++- tests/components/flux_led/test_config_flow.py | 26 +++--- .../forecast_solar/test_config_flow.py | 10 +-- tests/components/fritz/test_config_flow.py | 56 ++++++------ tests/components/fritzbox/test_config_flow.py | 68 +++++++------- .../fritzbox_callmonitor/test_config_flow.py | 34 ++++--- tests/components/fronius/test_config_flow.py | 32 +++---- .../garages_amsterdam/test_config_flow.py | 12 +-- .../components/geocaching/test_config_flow.py | 8 +- tests/components/github/test_config_flow.py | 19 ++-- .../components/gogogate2/test_config_flow.py | 32 +++---- tests/components/goodwe/test_config_flow.py | 18 ++-- tests/components/group/test_config_flow.py | 34 ++++--- .../components/hardkernel/test_config_flow.py | 6 +- .../here_travel_time/test_config_flow.py | 8 +- .../homeassistant_yellow/test_config_flow.py | 6 +- .../homekit_controller/test_config_flow.py | 6 +- .../components/homewizard/test_config_flow.py | 26 +++--- .../integration/test_config_flow.py | 10 +-- .../intellifire/test_config_flow.py | 34 ++++--- tests/components/iotawatt/test_config_flow.py | 18 ++-- tests/components/ipp/test_config_flow.py | 54 ++++++------ .../kaleidescape/test_config_flow.py | 24 +++-- tests/components/knx/test_config_flow.py | 88 +++++++++---------- .../components/laundrify/test_config_flow.py | 24 +++-- tests/components/lookin/test_config_flow.py | 20 ++--- .../components/luftdaten/test_config_flow.py | 26 +++--- tests/components/mill/test_config_flow.py | 32 +++---- tests/components/min_max/test_config_flow.py | 10 +-- .../minecraft_server/test_config_flow.py | 26 +++--- tests/components/mjpeg/test_config_flow.py | 38 ++++---- .../modern_forms/test_config_flow.py | 24 +++-- .../moehlenhoff_alpha2/test_config_flow.py | 16 ++-- tests/components/moon/test_config_flow.py | 14 ++- tests/components/mullvad/test_config_flow.py | 10 +-- tests/components/nzbget/test_config_flow.py | 24 +++-- tests/components/oncue/test_config_flow.py | 18 ++-- tests/components/onewire/test_config_flow.py | 16 ++-- tests/components/onewire/test_options_flow.py | 22 ++--- .../components/open_meteo/test_config_flow.py | 6 +- .../components/opengarage/test_config_flow.py | 18 ++-- .../components/p1_monitor/test_config_flow.py | 8 +- tests/components/peco/test_config_flow.py | 12 +-- tests/components/pi_hole/test_config_flow.py | 26 +++--- tests/components/plaato/test_config_flow.py | 38 ++++---- .../components/powerwall/test_config_flow.py | 8 +- .../progettihwsw/test_config_flow.py | 18 ++-- tests/components/prosegur/test_config_flow.py | 8 +- .../pure_energie/test_config_flow.py | 18 ++-- tests/components/pvoutput/test_config_flow.py | 34 ++++--- .../radio_browser/test_config_flow.py | 14 ++- .../rainforest_eagle/test_config_flow.py | 10 +-- .../raspberry_pi/test_config_flow.py | 6 +- tests/components/rdw/test_config_flow.py | 14 +-- tests/components/ridwell/test_config_flow.py | 16 ++-- tests/components/roku/test_config_flow.py | 38 ++++---- .../components/rpi_power/test_config_flow.py | 16 ++-- tests/components/sabnzbd/test_config_flow.py | 4 +- .../components/samsungtv/test_config_flow.py | 28 +++--- tests/components/season/test_config_flow.py | 14 ++- tests/components/senseme/test_config_flow.py | 46 +++++----- tests/components/sensibo/test_config_flow.py | 30 +++---- tests/components/sentry/test_config_flow.py | 18 ++-- tests/components/skybell/test_config_flow.py | 22 ++--- .../components/slimproto/test_config_flow.py | 6 +- tests/components/sma/test_config_flow.py | 20 ++--- tests/components/sonarr/test_config_flow.py | 32 +++---- tests/components/songpal/test_config_flow.py | 28 +++--- tests/components/sql/test_config_flow.py | 36 ++++---- .../components/squeezebox/test_config_flow.py | 30 +++---- tests/components/steamist/test_config_flow.py | 52 +++++------ .../components/stookalert/test_config_flow.py | 12 +-- tests/components/sun/test_config_flow.py | 14 ++- .../surepetcare/test_config_flow.py | 18 ++-- .../switch_as_x/test_config_flow.py | 10 +-- .../components/switchbot/test_config_flow.py | 28 +++--- .../switcher_kis/test_config_flow.py | 18 ++-- .../components/tailscale/test_config_flow.py | 32 +++---- .../tankerkoenig/test_config_flow.py | 38 ++++---- .../tesla_wall_connector/test_config_flow.py | 12 +-- .../components/threshold/test_config_flow.py | 14 +-- tests/components/tod/test_config_flow.py | 10 +-- tests/components/tolo/test_config_flow.py | 20 ++--- tests/components/tplink/test_config_flow.py | 14 +-- .../trafikverket_ferry/test_config_flow.py | 20 ++--- .../trafikverket_train/test_config_flow.py | 26 +++--- .../test_config_flow.py | 4 +- .../twentemilieu/test_config_flow.py | 20 ++--- .../ukraine_alarm/test_config_flow.py | 50 +++++------ .../unifiprotect/test_config_flow.py | 62 ++++++------- tests/components/uptime/test_config_flow.py | 14 ++- .../uptimerobot/test_config_flow.py | 36 ++++---- .../utility_meter/test_config_flow.py | 18 ++-- tests/components/vallox/test_config_flow.py | 36 ++++---- tests/components/venstar/test_config_flow.py | 18 ++-- tests/components/vera/test_config_flow.py | 10 +-- tests/components/verisure/test_config_flow.py | 38 ++++---- tests/components/version/test_config_flow.py | 30 +++---- .../components/vlc_telnet/test_config_flow.py | 28 +++--- tests/components/watttime/test_config_flow.py | 22 ++--- tests/components/webostv/test_config_flow.py | 42 ++++----- tests/components/whois/test_config_flow.py | 18 ++-- tests/components/wiffi/test_config_flow.py | 14 ++- tests/components/wilight/test_config_flow.py | 22 ++--- tests/components/wiz/test_config_flow.py | 16 ++-- .../yale_smart_alarm/test_config_flow.py | 32 +++---- tests/components/yeelight/test_config_flow.py | 32 +++---- tests/components/youless/test_config_flows.py | 10 +-- tests/components/zha/test_config_flow.py | 60 ++++++------- tests/components/zwave_me/test_config_flow.py | 25 +++--- tests/test_config_entries.py | 18 ++-- tests/test_data_entry_flow.py | 2 +- 142 files changed, 1463 insertions(+), 1849 deletions(-) diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 406e9a033da..15e831f551a 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -10,7 +10,7 @@ from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.components.http import CONF_TRUSTED_PROXIES, CONF_USE_X_FORWARDED_FOR -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -209,7 +209,7 @@ async def test_login_flow(manager, provider): # not from trusted network flow = await provider.async_login_flow({"ip_address": ip_address("127.0.0.1")}) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, list users @@ -224,7 +224,7 @@ async def test_login_flow(manager, provider): # login with valid user step = await flow.async_step_init({"user": user.id}) - assert step["type"] == RESULT_TYPE_CREATE_ENTRY + assert step["type"] == FlowResultType.CREATE_ENTRY assert step["data"]["user"] == user.id @@ -248,7 +248,7 @@ async def test_trusted_users_login(manager_with_user, provider_with_user): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, list users intersect trusted_users @@ -332,7 +332,7 @@ async def test_trusted_group_login(manager_with_user, provider_with_user): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, list users intersect trusted_users @@ -370,7 +370,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, only one available user, bypass the login flow @@ -378,7 +378,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): {"ip_address": ip_address("192.168.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_CREATE_ENTRY + assert step["type"] == FlowResultType.CREATE_ENTRY assert step["data"]["user"] == owner.id user = await manager_bypass_login.async_create_user("test-user") diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py index 998e4df8b15..0c12c486754 100644 --- a/tests/components/adax/test_config_flow.py +++ b/tests/components/adax/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.components.adax.const import ( ) from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -39,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("adax.get_adax_token", return_value="test_token",), patch( "homeassistant.components.adax.async_setup_entry", @@ -73,7 +73,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch( "adax.get_adax_token", @@ -83,7 +83,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result2["flow_id"], TEST_DATA, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -108,7 +108,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("adax.get_adax_token", return_value="token"): result3 = await hass.config_entries.flow.async_configure( @@ -129,7 +129,7 @@ async def test_local_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -138,7 +138,7 @@ async def test_local_create_entry(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -190,7 +190,7 @@ async def test_local_flow_entry_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -199,7 +199,7 @@ async def test_local_flow_entry_already_exists(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -228,7 +228,7 @@ async def test_local_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -237,7 +237,7 @@ async def test_local_connection_error(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -253,7 +253,7 @@ async def test_local_connection_error(hass): test_data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -263,7 +263,7 @@ async def test_local_heater_not_available(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -272,7 +272,7 @@ async def test_local_heater_not_available(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -288,7 +288,7 @@ async def test_local_heater_not_available(hass): test_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "heater_not_available" @@ -298,7 +298,7 @@ async def test_local_heater_not_found(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -307,7 +307,7 @@ async def test_local_heater_not_found(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -323,7 +323,7 @@ async def test_local_heater_not_found(hass): test_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "heater_not_found" @@ -333,7 +333,7 @@ async def test_local_invalid_wifi_cred(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -342,7 +342,7 @@ async def test_local_invalid_wifi_cred(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -358,5 +358,5 @@ async def test_local_invalid_wifi_cred(hass): test_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_auth" diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index 0ecb2c7a8dc..6e3a1579f70 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -6,7 +6,7 @@ import airthings from homeassistant import config_entries from homeassistant.components.airthings.const import CONF_ID, CONF_SECRET, DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("airthings.get_token", return_value="test_token",), patch( @@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Airthings" assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -56,7 +56,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -75,7 +75,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -94,7 +94,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 33117c64110..19017e79570 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant import config_entries from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +18,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -40,7 +36,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Aladdin Connect" assert result2["data"] == { CONF_USERNAME: "test-username", @@ -70,7 +66,7 @@ async def test_form_failed_auth( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -94,7 +90,7 @@ async def test_form_connection_timeout( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -153,7 +149,7 @@ async def test_import_flow_success( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Aladdin Connect" assert result2["data"] == { CONF_USERNAME: "test-user", @@ -185,7 +181,7 @@ async def test_reauth_flow( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -201,7 +197,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { CONF_USERNAME: "test-username", @@ -232,7 +228,7 @@ async def test_reauth_flow_auth_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.return_value = False with patch( @@ -251,7 +247,7 @@ async def test_reauth_flow_auth_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -278,7 +274,7 @@ async def test_reauth_flow_connnection_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.side_effect = ClientConnectionError @@ -292,5 +288,5 @@ async def test_reauth_flow_connnection_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/ambee/test_config_flow.py b/tests/components/ambee/test_config_flow.py index a6220418681..233bbaf232b 100644 --- a/tests/components/ambee/test_config_flow.py +++ b/tests/components/ambee/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.ambee.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +19,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -42,7 +38,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_API_KEY: "example", @@ -64,7 +60,7 @@ async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -82,7 +78,7 @@ async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_api_key"} assert "flow_id" in result2 @@ -102,7 +98,7 @@ async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Name" assert result3.get("data") == { CONF_API_KEY: "example", @@ -131,7 +127,7 @@ async def test_api_error(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} @@ -150,7 +146,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -165,7 +161,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_API_KEY: "other_key", @@ -196,7 +192,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -211,7 +207,7 @@ async def test_reauth_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_api_key"} assert "flow_id" in result2 @@ -227,7 +223,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("type") == FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_API_KEY: "other_key", @@ -267,6 +263,6 @@ async def test_reauth_api_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py index 98ed0f0abf0..1f3dec8d5e1 100644 --- a/tests/components/anthemav/test_config_flow.py +++ b/tests/components/anthemav/test_config_flow.py @@ -6,7 +6,7 @@ from anthemav.device_error import DeviceError from homeassistant import config_entries from homeassistant.components.anthemav.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form_with_valid_connection( @@ -16,7 +16,7 @@ async def test_form_with_valid_connection( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -33,7 +33,7 @@ async def test_form_with_valid_connection( await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == { "host": "1.1.1.1", "port": 14999, @@ -64,7 +64,7 @@ async def test_form_device_info_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_receive_deviceinfo"} @@ -88,7 +88,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -105,7 +105,7 @@ async def test_import_configuration( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "host": "1.1.1.1", "port": 14999, diff --git a/tests/components/aseko_pool_live/test_config_flow.py b/tests/components/aseko_pool_live/test_config_flow.py index 5ab85c61a8b..754c7fd0a3c 100644 --- a/tests/components/aseko_pool_live/test_config_flow.py +++ b/tests/components/aseko_pool_live/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries, setup from homeassistant.components.aseko_pool_live.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "aseko@example.com" assert result2["data"] == {CONF_ACCESS_TOKEN: "any_access_token"} assert len(mock_setup_entry.mock_calls) == 1 @@ -82,5 +82,5 @@ async def test_get_account_info_exceptions( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": reason} diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py index 8a5f6b6763f..ed98924e19d 100644 --- a/tests/components/aussie_broadband/test_config_flow.py +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.components.aussie_broadband.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .common import FAKE_DATA, FAKE_SERVICES @@ -25,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["errors"] is None with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( @@ -42,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -87,7 +83,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_ABORT + assert result4["type"] == FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 0 @@ -96,7 +92,7 @@ async def test_no_services(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["errors"] is None with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( @@ -111,7 +107,7 @@ async def test_no_services(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_services_found" assert len(mock_setup_entry.mock_calls) == 0 @@ -130,7 +126,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -148,7 +144,7 @@ async def test_form_network_issue(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -176,7 +172,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index 77a83a9673b..0df9ce480a7 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.baf.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_NAME, MOCK_UUID, MockBAFDevice @@ -45,7 +41,7 @@ async def test_form_user(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +59,7 @@ async def test_form_cannot_connect(hass): {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -79,7 +75,7 @@ async def test_form_unknown_exception(hass): {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -99,7 +95,7 @@ async def test_zeroconf_discovery(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -137,7 +133,7 @@ async def test_zeroconf_updates_existing_ip(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "127.0.0.1" @@ -157,7 +153,7 @@ async def test_zeroconf_rejects_ipv6(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" @@ -176,7 +172,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass): type="mock_type", ), ) - assert discovery_result["type"] == RESULT_TYPE_FORM + assert discovery_result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -194,7 +190,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py index 2b448a3df56..d6be9feb727 100644 --- a/tests/components/balboa/test_config_flow.py +++ b/tests/components/balboa/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +21,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -41,7 +37,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +58,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client: MagicMock) -> No TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -82,7 +78,7 @@ async def test_unknown_error(hass: HomeAssistant, client: MagicMock) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -94,7 +90,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -110,7 +106,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index b37c81407f7..2b1e4e9cd2e 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.canary.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_TIMEOUT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import USER_INPUT, _patch_async_setup, _patch_async_setup_entry, init_integration @@ -26,7 +22,7 @@ async def test_user_form(hass, canary_config_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: @@ -36,7 +32,7 @@ async def test_user_form(hass, canary_config_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**USER_INPUT, CONF_TIMEOUT: DEFAULT_TIMEOUT} @@ -57,7 +53,7 @@ async def test_user_form_cannot_connect(hass, canary_config_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} canary_config_flow.side_effect = ConnectTimeout() @@ -67,7 +63,7 @@ async def test_user_form_cannot_connect(hass, canary_config_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -84,7 +80,7 @@ async def test_user_form_unexpected_exception(hass, canary_config_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -97,7 +93,7 @@ async def test_user_form_single_instance_allowed(hass, canary_config_flow): context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -110,7 +106,7 @@ async def test_options_flow(hass, canary): assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with _patch_async_setup(), _patch_async_setup_entry(): @@ -120,6 +116,6 @@ async def test_options_flow(hass, canary): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "-v" assert result["data"][CONF_TIMEOUT] == 7 diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index df8cff0f3a4..3d225e75803 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -8,11 +8,7 @@ from pycfdns.exceptions import ( from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE, CONF_ZONE -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( ENTRY_CONFIG, @@ -31,7 +27,7 @@ async def test_user_form(hass, cfupdate_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -41,7 +37,7 @@ async def test_user_form(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "zone" assert result["errors"] == {} @@ -51,7 +47,7 @@ async def test_user_form(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "records" assert result["errors"] is None @@ -62,7 +58,7 @@ async def test_user_form(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_ZONE[CONF_ZONE] assert result["data"] @@ -90,7 +86,7 @@ async def test_user_form_cannot_connect(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -108,7 +104,7 @@ async def test_user_form_invalid_auth(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -126,7 +122,7 @@ async def test_user_form_invalid_zone(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_zone"} @@ -144,7 +140,7 @@ async def test_user_form_unexpected_exception(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -158,7 +154,7 @@ async def test_user_form_single_instance_allowed(hass): context={CONF_SOURCE: SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -176,7 +172,7 @@ async def test_reauth_flow(hass, cfupdate_flow): }, data=entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with _patch_async_setup_entry() as mock_setup_entry: @@ -186,7 +182,7 @@ async def test_reauth_flow(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_TOKEN] == "other_token" diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 7961e413135..d85279fc30c 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.co2signal import DOMAIN, config_flow from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import VALID_PAYLOAD @@ -17,7 +17,7 @@ async def test_form_home(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( @@ -33,7 +33,7 @@ async def test_form_home(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "CO2 Signal" assert result2["data"] == { "api_key": "api_key", @@ -47,7 +47,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -57,7 +57,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( "homeassistant.components.co2signal.async_setup_entry", @@ -72,7 +72,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "12.3, 45.6" assert result3["data"] == { "latitude": 12.3, @@ -88,7 +88,7 @@ async def test_form_country(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -98,7 +98,7 @@ async def test_form_country(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( "homeassistant.components.co2signal.async_setup_entry", @@ -112,7 +112,7 @@ async def test_form_country(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "fr" assert result3["data"] == { "country_code": "fr", @@ -147,7 +147,7 @@ async def test_form_error_handling(hass: HomeAssistant, err_str, err_code) -> No }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": err_code} @@ -169,7 +169,7 @@ async def test_form_error_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -191,5 +191,5 @@ async def test_form_error_unexpected_data(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/cpuspeed/test_config_flow.py b/tests/components/cpuspeed/test_config_flow.py index 8f12092f389..c016cfdb1d9 100644 --- a/tests/components/cpuspeed/test_config_flow.py +++ b/tests/components/cpuspeed/test_config_flow.py @@ -5,11 +5,7 @@ from unittest.mock import AsyncMock, MagicMock from homeassistant.components.cpuspeed.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -24,7 +20,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +29,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "CPU Speed" assert result2.get("data") == {} @@ -54,7 +50,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -71,7 +67,7 @@ async def test_not_compatible( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -81,7 +77,7 @@ async def test_not_compatible( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "not_compatible" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 3a6a56fe097..bd118884edc 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components import zeroconf from homeassistant.components.daikin.const import KEY_MAC from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -54,7 +50,7 @@ async def test_user(hass, mock_daikin): context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -62,7 +58,7 @@ async def test_user(hass, mock_daikin): context={"source": SOURCE_USER}, data={CONF_HOST: HOST}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][KEY_MAC] == MAC @@ -77,7 +73,7 @@ async def test_abort_if_already_setup(hass, mock_daikin): data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -100,7 +96,7 @@ async def test_device_abort(hass, mock_daikin, s_effect, reason): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": reason} assert result["step_id"] == "user" @@ -112,7 +108,7 @@ async def test_api_password_abort(hass): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_API_KEY: "aa", CONF_PASSWORD: "aa"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "api_password"} assert result["step_id"] == "user" @@ -144,7 +140,7 @@ async def test_discovery_zeroconf( context={"source": source}, data=data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" MockConfigEntry(domain="daikin", unique_id=unique_id).add_to_hass(hass) @@ -154,7 +150,7 @@ async def test_discovery_zeroconf( data={CONF_HOST: HOST}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -163,5 +159,5 @@ async def test_discovery_zeroconf( data=data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py index b56c717e635..a32b72704e8 100644 --- a/tests/components/deluge/test_config_flow.py +++ b/tests/components/deluge/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.deluge.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import CONF_DATA @@ -60,7 +56,7 @@ async def test_flow_user(hass: HomeAssistant, api): context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -78,7 +74,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant, api): DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -87,7 +83,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, conn_error): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -97,7 +93,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown_error): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -121,13 +117,13 @@ async def test_flow_reauth(hass: HomeAssistant, api): data=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index 61ab7251f8a..b6fa62e290f 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.derivative.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My derivative" assert result["data"] == {} assert result["options"] == { @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -115,7 +115,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "unit_time": "h", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "My derivative", "round": 2.0, diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 7cd1ba5222c..f9d589eb638 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -16,11 +16,7 @@ from homeassistant.components.devolo_home_network.const import ( ) from homeassistant.const import CONF_BASE, CONF_IP_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .const import DISCOVERY_INFO, DISCOVERY_INFO_WRONG_DEVICE, IP @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -45,7 +41,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["result"].unique_id == info["serial_number"] assert result2["title"] == info["title"] assert result2["data"] == { @@ -75,7 +71,7 @@ async def test_form_error(hass: HomeAssistant, exception_type, expected_error): }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {CONF_BASE: expected_error} @@ -88,7 +84,7 @@ async def test_zeroconf(hass: HomeAssistant): ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {"host_name": "test"} context = next( @@ -125,7 +121,7 @@ async def test_abort_zeroconf_wrong_device(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO_WRONG_DEVICE, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "home_control" @@ -158,7 +154,7 @@ async def test_abort_if_configued(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" # Abort on concurrent zeroconf discovery flow @@ -167,7 +163,7 @@ async def test_abort_if_configued(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO, ) - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index f80e0d781ef..7ef6bdde69d 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_SERIAL from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.components.directv import ( HOST, @@ -35,7 +31,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_show_ssdp_form( @@ -49,7 +45,7 @@ async def test_show_ssdp_form( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -67,7 +63,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -85,7 +81,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -102,7 +98,7 @@ async def test_ssdp_confirm_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -119,7 +115,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -136,7 +132,7 @@ async def test_ssdp_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -154,7 +150,7 @@ async def test_ssdp_with_receiver_id_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -173,7 +169,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -192,7 +188,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -211,7 +207,7 @@ async def test_ssdp_confirm_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -226,7 +222,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -236,7 +232,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] @@ -255,7 +251,7 @@ async def test_full_ssdp_flow_implementation( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -263,7 +259,7 @@ async def test_full_ssdp_flow_implementation( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 51e169b8bb5..fdec45be7f5 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -18,11 +18,7 @@ from homeassistant.components.dnsip.const import ( ) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -66,7 +62,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -108,7 +104,7 @@ async def test_form_adv(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -141,7 +137,7 @@ async def test_form_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_hostname"} @@ -183,7 +179,7 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -217,7 +213,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -228,7 +224,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "resolver": "8.8.8.8", "resolver_ipv6": "2001:4860:4860::8888", @@ -287,7 +283,7 @@ async def test_options_error(hass: HomeAssistant, p_input: dict[str, str]) -> No ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "init" if p_input[CONF_IPV4]: assert result2["errors"] == {"resolver": "invalid_resolver"} diff --git a/tests/components/econet/test_config_flow.py b/tests/components/econet/test_config_flow.py index 68eb18a931e..635b9e2e40a 100644 --- a/tests/components/econet/test_config_flow.py +++ b/tests/components/econet/test_config_flow.py @@ -7,11 +7,7 @@ from pyeconet.errors import InvalidCredentialsError, PyeconetError from homeassistant.components.econet import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +18,7 @@ async def test_bad_credentials(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -39,7 +35,7 @@ async def test_bad_credentials(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "invalid_auth", @@ -52,7 +48,7 @@ async def test_generic_error_from_library(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -69,7 +65,7 @@ async def test_generic_error_from_library(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "cannot_connect", @@ -82,7 +78,7 @@ async def test_auth_worked(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -99,7 +95,7 @@ async def test_auth_worked(hass): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EMAIL: "admin@localhost.com", CONF_PASSWORD: "password0", @@ -119,7 +115,7 @@ async def test_already_configured(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -136,5 +132,5 @@ async def test_already_configured(hass): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/efergy/test_config_flow.py b/tests/components/efergy/test_config_flow.py index 89f3b266b7c..95effbfbd72 100644 --- a/tests/components/efergy/test_config_flow.py +++ b/tests/components/efergy/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.efergy.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import CONF_DATA, HID, _patch_efergy, _patch_efergy_status, create_entry @@ -27,14 +23,14 @@ async def test_flow_user(hass: HomeAssistant): DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == HID @@ -47,7 +43,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -59,7 +55,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -71,7 +67,7 @@ async def test_flow_user_unknown(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -90,7 +86,7 @@ async def test_flow_reauth(hass: HomeAssistant): data=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" new_conf = {CONF_API_KEY: "1234567890"} @@ -98,6 +94,6 @@ async def test_flow_reauth(hass: HomeAssistant): result["flow_id"], user_input=new_conf, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf diff --git a/tests/components/eight_sleep/test_config_flow.py b/tests/components/eight_sleep/test_config_flow.py index 8015fb6c69d..1cace5b31cd 100644 --- a/tests/components/eight_sleep/test_config_flow.py +++ b/tests/components/eight_sleep/test_config_flow.py @@ -1,11 +1,7 @@ """Test the Eight Sleep config flow.""" from homeassistant import config_entries from homeassistant.components.eight_sleep.const import DOMAIN -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass) -> None: @@ -13,7 +9,7 @@ async def test_form(hass) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -24,7 +20,7 @@ async def test_form(hass) -> None: }, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -37,7 +33,7 @@ async def test_form_invalid_auth(hass, token_error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -48,7 +44,7 @@ async def test_form_invalid_auth(hass, token_error) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -63,7 +59,7 @@ async def test_import(hass) -> None: }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == { "username": "test-username", @@ -81,5 +77,5 @@ async def test_import_invalid_auth(hass, token_error) -> None: "password": "bad-password", }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index 0e3916a005e..dbdfbfed1d0 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.elgato.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -28,7 +24,7 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -36,7 +32,7 @@ async def test_full_user_flow_implementation( result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 9123} ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "CN11A1A00001" assert result2.get("data") == { CONF_HOST: "127.0.0.1", @@ -72,7 +68,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {"serial_number": "CN11A1A00001"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result progress = hass.config_entries.flow.async_progress() @@ -85,7 +81,7 @@ async def test_full_zeroconf_flow_implementation( result["flow_id"], user_input={} ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "CN11A1A00001" assert result2.get("data") == { CONF_HOST: "127.0.0.1", @@ -111,7 +107,7 @@ async def test_connection_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert result.get("step_id") == "user" @@ -137,7 +133,7 @@ async def test_zeroconf_connection_error( ) assert result.get("reason") == "cannot_connect" - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT async def test_user_device_exists_abort( @@ -153,7 +149,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -178,7 +174,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -199,7 +195,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -227,7 +223,7 @@ async def test_zeroconf_during_onboarding( ), ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "CN11A1A00001" assert result.get("data") == { CONF_HOST: "127.0.0.1", diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 398dfd30d03..e47dc402b64 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.elkm1.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( ELK_DISCOVERY, @@ -47,7 +47,7 @@ async def test_discovery_ignored_entry(hass): data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -242,7 +242,7 @@ async def test_form_user_with_insecure_elk_times_out(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -280,7 +280,7 @@ async def test_form_user_with_secure_elk_no_discovery_ip_already_configured(hass ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "address_already_configured" @@ -961,7 +961,7 @@ async def test_form_import_existing(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "address_already_configured" @@ -989,7 +989,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == "cc:cc:cc:cc:cc:cc" @@ -1018,7 +1018,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MOCK_MAC @@ -1034,7 +1034,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(), _patch_elk(): @@ -1044,7 +1044,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_elk(): @@ -1058,7 +1058,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -1073,7 +1073,7 @@ async def test_discovered_by_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1118,7 +1118,7 @@ async def test_discovered_by_discovery_non_standard_port(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1169,7 +1169,7 @@ async def test_discovered_by_discovery_url_already_configured(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -1182,7 +1182,7 @@ async def test_discovered_by_dhcp_udp_responds(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1225,7 +1225,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1275,7 +1275,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1315,5 +1315,5 @@ async def test_discovered_by_dhcp_no_udp_response(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 1d2cff051ae..43e2f916082 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -15,11 +15,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -66,7 +62,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): data=None, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) @@ -77,7 +73,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 80, @@ -109,7 +105,7 @@ async def test_user_resolve_error(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "resolve_error"} @@ -128,7 +124,7 @@ async def test_user_connection_error(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} @@ -147,14 +143,14 @@ async def test_user_with_password(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password1"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -174,7 +170,7 @@ async def test_user_invalid_password(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = InvalidAuthAPIError @@ -183,7 +179,7 @@ async def test_user_invalid_password(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_PASSWORD: "invalid"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "invalid_auth"} @@ -198,7 +194,7 @@ async def test_login_connection_error(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = APIConnectionError @@ -207,7 +203,7 @@ async def test_login_connection_error(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_PASSWORD: "valid"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "connection_error"} @@ -233,7 +229,7 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): flow["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test8266" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -264,7 +260,7 @@ async def test_discovery_already_configured_hostname(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -292,7 +288,7 @@ async def test_discovery_already_configured_ip(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -324,7 +320,7 @@ async def test_discovery_already_configured_name(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -348,13 +344,13 @@ async def test_discovery_duplicate_data(hass, mock_client): result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -380,7 +376,7 @@ async def test_discovery_updates_unique_id(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -396,7 +392,7 @@ async def test_user_requires_psk(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {} @@ -416,7 +412,7 @@ async def test_encryption_key_valid_psk(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) @@ -424,7 +420,7 @@ async def test_encryption_key_valid_psk(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -445,7 +441,7 @@ async def test_encryption_key_invalid_psk(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError @@ -453,7 +449,7 @@ async def test_encryption_key_invalid_psk(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {"base": "invalid_psk"} assert mock_client.noise_psk == INVALID_NOISE_PSK @@ -475,7 +471,7 @@ async def test_reauth_initiation(hass, mock_client, mock_zeroconf): "unique_id": entry.unique_id, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -501,7 +497,7 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -528,7 +524,7 @@ async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" @@ -556,7 +552,7 @@ async def test_discovery_dhcp_updates_host(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -585,7 +581,7 @@ async def test_discovery_dhcp_no_changes(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" diff --git a/tests/components/evil_genius_labs/test_config_flow.py b/tests/components/evil_genius_labs/test_config_flow.py index e3354f4b9cc..19434c77d3c 100644 --- a/tests/components/evil_genius_labs/test_config_flow.py +++ b/tests/components/evil_genius_labs/test_config_flow.py @@ -7,7 +7,7 @@ import aiohttp from homeassistant import config_entries from homeassistant.components.evil_genius_labs.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form( @@ -17,7 +17,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -41,7 +41,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Fibonacci256-23D4" assert result2["data"] == { "host": "1.1.1.1", @@ -66,7 +66,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, caplog) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert "Unable to connect" in caplog.text @@ -88,7 +88,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "timeout"} @@ -109,5 +109,5 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index 4ba3b5911f6..c31a43c1949 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -29,11 +29,7 @@ from homeassistant.const import ( CONF_URL, CONF_USERNAME, ) -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( DISCOVERY_INFO, @@ -50,7 +46,7 @@ async def test_user_form(hass, ezviz_config_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -61,7 +57,7 @@ async def test_user_form(hass, ezviz_config_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**USER_INPUT} @@ -70,7 +66,7 @@ async def test_user_form(hass, ezviz_config_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured_account" @@ -85,7 +81,7 @@ async def test_user_custom_url(hass, ezviz_config_flow): {CONF_USERNAME: "test-user", CONF_PASSWORD: "test-pass", CONF_URL: "customize"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -95,7 +91,7 @@ async def test_user_custom_url(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_PASSWORD: "test-pass", CONF_TYPE: ATTR_TYPE_CLOUD, @@ -112,7 +108,7 @@ async def test_step_discovery_abort_if_cloud_account_missing(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -125,7 +121,7 @@ async def test_step_discovery_abort_if_cloud_account_missing(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ezviz_cloud_account_missing" @@ -139,7 +135,7 @@ async def test_async_step_integration_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -153,7 +149,7 @@ async def test_async_step_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_PASSWORD: "test-pass", CONF_TYPE: ATTR_TYPE_CAMERA, @@ -172,7 +168,7 @@ async def test_options_flow(hass): assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -182,7 +178,7 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "/H.264" assert result["data"][CONF_TIMEOUT] == 25 @@ -202,7 +198,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -213,7 +209,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -224,7 +220,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -235,7 +231,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -252,7 +248,7 @@ async def test_discover_exception_step1( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -267,7 +263,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -281,7 +277,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -295,7 +291,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -309,7 +305,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -327,7 +323,7 @@ async def test_discover_exception_step3( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -342,7 +338,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -356,7 +352,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -370,7 +366,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -390,7 +386,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -399,7 +395,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_auth"} @@ -410,7 +406,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_host"} @@ -421,7 +417,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "cannot_connect"} @@ -432,5 +428,5 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index 16f0ab38dc1..ed9d4004b1a 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -5,11 +5,7 @@ from homeassistant.components.filesize.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import TEST_DIR, TEST_FILE, TEST_FILE_NAME, async_create_file @@ -24,7 +20,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={CONF_FILE_PATH: TEST_FILE}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == TEST_FILE_NAME assert result2.get("data") == {CONF_FILE_PATH: TEST_FILE} @@ -51,7 +47,7 @@ async def test_unique_path( DOMAIN, context={"source": SOURCE_USER}, data={CONF_FILE_PATH: TEST_FILE} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -64,7 +60,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER result2 = await hass.config_entries.flow.async_configure( @@ -103,7 +99,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_FILE_NAME assert result2["data"] == { CONF_FILE_PATH: TEST_FILE, diff --git a/tests/components/fivem/test_config_flow.py b/tests/components/fivem/test_config_flow.py index a1a1e8f2a37..121b416a110 100644 --- a/tests/components/fivem/test_config_flow.py +++ b/tests/components/fivem/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.fivem.config_flow import DEFAULT_PORT from homeassistant.components.fivem.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType USER_INPUT = { CONF_HOST: "fivem.dummyserver.com", @@ -54,7 +54,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -63,7 +63,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -79,7 +79,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == USER_INPUT[CONF_HOST] assert result2["data"] == USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -101,7 +101,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -121,7 +121,7 @@ async def test_form_invalid(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -141,5 +141,5 @@ async def test_form_invalid_game_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_game_name"} diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index 22808382c49..a51b8c6f9fa 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -9,11 +9,7 @@ from pytest import fixture from homeassistant import config_entries from homeassistant.components.fjaraskupan.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType @fixture(name="mock_setup_entry", autouse=True) @@ -32,10 +28,10 @@ async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Fjäråskupan" assert result["data"] == {} @@ -51,8 +47,8 @@ async def test_scan_no_devices(hass: HomeAssistant, scanner: list[BLEDevice]) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 9750600518e..8abdb8e955b 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -24,7 +24,7 @@ from homeassistant.components.flux_led.const import ( ) from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_TITLE, @@ -386,7 +386,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=FLUX_DISCOVERY, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(): @@ -396,7 +396,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_wifibulb(): @@ -410,7 +410,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -425,7 +425,7 @@ async def test_discovered_by_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(), patch( @@ -462,7 +462,7 @@ async def test_discovered_by_dhcp_udp_responds(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(), patch( @@ -499,7 +499,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(no_device=True), _patch_wifibulb(), patch( @@ -529,7 +529,7 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(device=FLUX_DISCOVERY_PARTIAL), _patch_wifibulb(), patch( @@ -560,7 +560,7 @@ async def test_discovered_by_dhcp_no_udp_response_or_tcp_response(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -584,7 +584,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -605,7 +605,7 @@ async def test_mac_address_off_by_one_updated_via_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -624,7 +624,7 @@ async def test_mac_address_off_by_one_not_updated_from_dhcp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_ONE_OFF @@ -652,7 +652,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_DIFFERENT diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index f0611d1678d..2380e65aabb 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components.forecast_solar.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +23,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -42,7 +42,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_LATITUDE: 52.42, @@ -70,7 +70,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -86,7 +86,7 @@ async def test_options_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_API_KEY: "solarPOWER!", CONF_DECLINATION: 21, diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 619f06f5493..76f556d0743 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -18,11 +18,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .const import ( MOCK_FIRMWARE_INFO, @@ -62,13 +58,13 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -113,13 +109,13 @@ async def test_user_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "already_configured" @@ -130,7 +126,7 @@ async def test_exception_security(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -142,7 +138,7 @@ async def test_exception_security(hass: HomeAssistant, mock_get_source_ip): result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_AUTH_INVALID @@ -153,7 +149,7 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -165,7 +161,7 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip): result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_CANNOT_CONNECT @@ -176,7 +172,7 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -188,7 +184,7 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip): result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_UNKNOWN @@ -226,7 +222,7 @@ async def test_reauth_successful( data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -237,7 +233,7 @@ async def test_reauth_successful( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_setup_entry.called @@ -262,7 +258,7 @@ async def test_reauth_not_successful( data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -273,7 +269,7 @@ async def test_reauth_not_successful( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "cannot_connect" @@ -301,7 +297,7 @@ async def test_ssdp_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -328,7 +324,7 @@ async def test_ssdp_already_configured_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -355,7 +351,7 @@ async def test_ssdp_already_configured_host_uuid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -371,7 +367,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA) @@ -380,7 +376,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -408,7 +404,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -419,7 +415,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == MOCK_IPS["fritz.box"] assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -437,7 +433,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -448,7 +444,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" @@ -463,7 +459,7 @@ async def test_options_flow(hass: HomeAssistant, fc_class_mock, mock_get_source_ side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzBoxTools"): result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_init(mock_config.entry_id) @@ -473,5 +469,5 @@ async def test_options_flow(hass: HomeAssistant, fc_class_mock, mock_get_source_ CONF_CONSIDER_HOME: 37, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert mock_config.options[CONF_CONSIDER_HOME] == 37 diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 442b2f4d568..e82101b3977 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .const import CONF_FAKE_NAME, MOCK_CONFIG @@ -70,13 +66,13 @@ async def test_user(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -91,7 +87,7 @@ async def test_user_auth_failed(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -103,7 +99,7 @@ async def test_user_not_successful(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -112,13 +108,13 @@ async def test_user_already_configured(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -132,7 +128,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -143,7 +139,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_config.data[CONF_USERNAME] == "other_fake_user" assert mock_config.data[CONF_PASSWORD] == "other_fake_password" @@ -161,7 +157,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -172,7 +168,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -189,7 +185,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -200,16 +196,16 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @pytest.mark.parametrize( "test_data,expected_result", [ - (MOCK_SSDP_DATA["ip4_valid"], RESULT_TYPE_FORM), - (MOCK_SSDP_DATA["ip6_valid"], RESULT_TYPE_FORM), - (MOCK_SSDP_DATA["ip6_invalid"], RESULT_TYPE_ABORT), + (MOCK_SSDP_DATA["ip4_valid"], FlowResultType.FORM), + (MOCK_SSDP_DATA["ip6_valid"], FlowResultType.FORM), + (MOCK_SSDP_DATA["ip6_invalid"], FlowResultType.ABORT), ], ) async def test_ssdp( @@ -224,7 +220,7 @@ async def test_ssdp( ) assert result["type"] == expected_result - if expected_result == RESULT_TYPE_ABORT: + if expected_result == FlowResultType.ABORT: return assert result["step_id"] == "confirm" @@ -233,7 +229,7 @@ async def test_ssdp( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == CONF_FAKE_NAME assert result["data"][CONF_HOST] == urlparse(test_data.ssdp_location).hostname assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -249,14 +245,14 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_NAME ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -271,7 +267,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -279,7 +275,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock): result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"]["base"] == "invalid_auth" @@ -291,14 +287,14 @@ async def test_ssdp_not_successful(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -309,14 +305,14 @@ async def test_ssdp_not_supported(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -325,13 +321,13 @@ async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistant, fritz: Mo result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -340,7 +336,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA["ip4_valid"]) @@ -349,7 +345,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -358,12 +354,12 @@ async def test_ssdp_already_configured(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" assert result["result"].unique_id == "only-a-test" diff --git a/tests/components/fritzbox_callmonitor/test_config_flow.py b/tests/components/fritzbox_callmonitor/test_config_flow.py index 51884abe96f..94d5bdc8eeb 100644 --- a/tests/components/fritzbox_callmonitor/test_config_flow.py +++ b/tests/components/fritzbox_callmonitor/test_config_flow.py @@ -22,11 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, patch @@ -74,7 +70,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -104,7 +100,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_1 assert result["data"] == MOCK_CONFIG_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -116,7 +112,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -140,7 +136,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "phonebook" assert result["errors"] == {} @@ -156,7 +152,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: {CONF_PHONEBOOK: MOCK_PHONEBOOK_NAME_2}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_2 assert result["data"] == { CONF_HOST: MOCK_HOST, @@ -184,7 +180,7 @@ async def test_setup_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == ConnectResult.NO_DEVIES_FOUND @@ -203,7 +199,7 @@ async def test_setup_insufficient_permissions(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == ConnectResult.INSUFFICIENT_PERMISSIONS @@ -222,7 +218,7 @@ async def test_setup_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.INVALID_AUTH} @@ -244,14 +240,14 @@ async def test_options_flow_correct_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_PREFIXES: "+49, 491234"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: ["+49", "491234"]} @@ -273,14 +269,14 @@ async def test_options_flow_incorrect_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_PREFIXES: ""} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.MALFORMED_PREFIXES} @@ -302,12 +298,12 @@ async def test_options_flow_no_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: None} diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index 256d64d4cbe..69f1dffa64b 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fronius.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import mock_responses @@ -51,7 +47,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -69,7 +65,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Datalogger at 10.9.8.1" assert result2["data"] == { "host": "10.9.8.1", @@ -83,7 +79,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -104,7 +100,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Inverter at 10.9.1.1" assert result2["data"] == { "host": "10.9.1.1", @@ -133,7 +129,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -157,7 +153,7 @@ async def test_form_no_device(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -178,7 +174,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -206,7 +202,7 @@ async def test_form_already_existing(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -246,7 +242,7 @@ async def test_form_updates_host(hass, aioclient_mock): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" mock_unload_entry.assert_called_with(hass, entry) @@ -269,13 +265,13 @@ async def test_dhcp(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm_discovery" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"SolarNet Datalogger at {MOCK_DHCP_DATA.ip}" assert result["data"] == { "host": MOCK_DHCP_DATA.ip, @@ -298,7 +294,7 @@ async def test_dhcp_already_configured(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -313,5 +309,5 @@ async def test_dhcp_invalid(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_host" diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index 3749cf039db..3dd9c67bc15 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -8,11 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.garages_amsterdam.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_full_flow(hass: HomeAssistant) -> None: @@ -21,7 +17,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result with patch( @@ -34,7 +30,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "IJDok" assert "result" in result2 assert result2["result"].unique_id == "IJDok" @@ -63,5 +59,5 @@ async def test_error_handling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == reason diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index 56a301e2f3c..a40668c627f 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -17,7 +17,7 @@ from homeassistant.components.geocaching.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_EXTERNAL_STEP +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -63,7 +63,7 @@ async def test_full_flow( }, ) - assert result.get("type") == RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == FlowResultType.EXTERNAL_STEP assert result.get("step_id") == "auth" assert result.get("url") == ( f"{CURRENT_ENVIRONMENT_URLS['authorize_url']}?response_type=code&client_id={CLIENT_ID}" @@ -161,7 +161,7 @@ async def test_oauth_error( "redirect_uri": REDIRECT_URI, }, ) - assert result.get("type") == RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == FlowResultType.EXTERNAL_STEP client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -181,7 +181,7 @@ async def test_oauth_error( ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "oauth_error" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 2bf0fac209f..174d70c3ae3 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -12,12 +12,7 @@ from homeassistant.components.github.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_SHOW_PROGRESS, - RESULT_TYPE_SHOW_PROGRESS_DONE, -) +from homeassistant.data_entry_flow import FlowResultType from .common import MOCK_ACCESS_TOKEN @@ -63,7 +58,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "device" - assert result["type"] == RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == FlowResultType.SHOW_PROGRESS assert "flow_id" in result result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -76,7 +71,7 @@ async def test_full_user_flow_implementation( ) assert result["title"] == "" - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_ACCESS_TOKEN] == MOCK_ACCESS_TOKEN assert "options" in result @@ -96,7 +91,7 @@ async def test_flow_with_registration_failure( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result.get("reason") == "could_not_register" @@ -125,10 +120,10 @@ async def test_flow_with_activation_failure( context={"source": config_entries.SOURCE_USER}, ) assert result["step_id"] == "device" - assert result["type"] == RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE assert result["step_id"] == "could_not_register" @@ -144,7 +139,7 @@ async def test_already_configured( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 713295c0efd..cbe40a2b9cb 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -20,11 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import _mocked_ismartgate_closed_door_response @@ -59,7 +55,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == { "base": "invalid_auth", } @@ -79,7 +75,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -97,7 +93,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -117,7 +113,7 @@ async def test_form_homekit_unique_id_already_setup(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} flow = next( flow @@ -145,7 +141,7 @@ async def test_form_homekit_unique_id_already_setup(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT async def test_form_homekit_ip_address_already_setup(hass): @@ -170,7 +166,7 @@ async def test_form_homekit_ip_address_already_setup(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT async def test_form_homekit_ip_address(hass): @@ -189,7 +185,7 @@ async def test_form_homekit_ip_address(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} data_schema = result["data_schema"] @@ -219,7 +215,7 @@ async def test_discovered_dhcp( ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -231,7 +227,7 @@ async def test_discovered_dhcp( }, ) assert result2 - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -247,7 +243,7 @@ async def test_discovered_dhcp( }, ) assert result3 - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["data"] == { "device": "ismartgate", "ip_address": "1.2.3.4", @@ -272,7 +268,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_init( @@ -282,7 +278,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" result3 = await hass.config_entries.flow.async_init( @@ -292,5 +288,5 @@ async def test_discovered_by_homekit_and_dhcp(hass): ip="1.2.3.4", macaddress="00:00:00:00:00:00", hostname="mock_hostname" ), ) - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" diff --git a/tests/components/goodwe/test_config_flow.py b/tests/components/goodwe/test_config_flow.py index 89dfd68a783..fe1af93a472 100644 --- a/tests/components/goodwe/test_config_flow.py +++ b/tests/components/goodwe/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.goodwe.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -35,7 +31,7 @@ async def test_manual_setup(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -50,7 +46,7 @@ async def test_manual_setup(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -68,7 +64,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -81,7 +77,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -90,7 +86,7 @@ async def test_manual_setup_device_offline(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -103,5 +99,5 @@ async def test_manual_setup_device_offline(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_error"} diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 9d6b099557d..4c73e1d5add 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -6,11 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.group import DOMAIN, async_setup_entry from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, - RESULT_TYPE_MENU, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -47,14 +43,14 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_MENU + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type with patch( @@ -70,7 +66,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room" assert result["data"] == {} assert result["options"] == { @@ -136,14 +132,14 @@ async def test_config_flow_hides_members( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_MENU + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.flow.async_configure( @@ -157,7 +153,7 @@ async def test_config_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by @@ -220,7 +216,7 @@ async def test_options( config_entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") == members1 assert "name" not in result["data_schema"].schema @@ -234,7 +230,7 @@ async def test_options( "entities": members2, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -261,14 +257,14 @@ async def test_options( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_MENU + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") is None @@ -318,7 +314,7 @@ async def test_all_options( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": advanced} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.options.async_configure( @@ -327,7 +323,7 @@ async def test_all_options( "entities": members2, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -414,7 +410,7 @@ async def test_options_flow_hides_members( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(group_config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -425,7 +421,7 @@ async def test_options_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by diff --git a/tests/components/hardkernel/test_config_flow.py b/tests/components/hardkernel/test_config_flow.py index f74b4a4e658..309c796fcc3 100644 --- a/tests/components/hardkernel/test_config_flow.py +++ b/tests/components/hardkernel/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.hardkernel.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Hardkernel" assert result["data"] == {} assert result["options"] == {} @@ -53,6 +53,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index dc9eedfa3f6..23e3c1c81c7 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -154,7 +154,7 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result2["type"] == data_entry_flow.FlowResultType.MENU assert result2["menu_options"] == menu_options @@ -178,7 +178,7 @@ async def test_step_origin_coordinates( } }, ) - assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert location_selector_result["type"] == data_entry_flow.FlowResultType.MENU @pytest.mark.usefixtures("valid_response") @@ -195,7 +195,7 @@ async def test_step_origin_entity( menu_result["flow_id"], {"origin_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert entity_selector_result["type"] == data_entry_flow.FlowResultType.MENU @pytest.mark.usefixtures("valid_response") @@ -341,7 +341,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result["type"] == data_entry_flow.FlowResultType.MENU @pytest.mark.usefixtures("valid_response") diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 2e96b05a919..e6d5da11806 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow" assert result["data"] == {} assert result["options"] == {} @@ -53,6 +53,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 3b106dab186..170a2a797d0 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry from tests.common import MockConfigEntry, mock_device_registry @@ -943,10 +943,10 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info_paired, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_paired" mdns_update_to_paired.set() result = await task - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == {} diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index fca00b71892..cdd67cbf7ea 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homewizard.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .generator import get_mock_device @@ -95,7 +91,7 @@ async def test_discovery_flow_works(hass, aioclient_mock): result = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input=None ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with patch( @@ -109,7 +105,7 @@ async def test_discovery_flow_works(hass, aioclient_mock): flow["flow_id"], user_input={"ip_address": "192.168.43.183"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "P1 meter (aabbccddeeff)" assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" @@ -176,7 +172,7 @@ async def test_discovery_disabled_api(hass, aioclient_mock): data=service_info, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.homewizard.async_setup_entry", @@ -189,7 +185,7 @@ async def test_discovery_disabled_api(hass, aioclient_mock): result["flow_id"], user_input={"ip_address": "192.168.43.183"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "api_not_enabled" @@ -218,7 +214,7 @@ async def test_discovery_missing_data_in_service_info(hass, aioclient_mock): data=service_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_discovery_parameters" @@ -247,7 +243,7 @@ async def test_discovery_invalid_api(hass, aioclient_mock): data=service_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unsupported_api_version" @@ -275,7 +271,7 @@ async def test_check_disabled_api(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "api_not_enabled" @@ -303,7 +299,7 @@ async def test_check_error_handling_api(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown_error" @@ -331,7 +327,7 @@ async def test_check_detects_invalid_api(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unsupported_api_version" @@ -359,5 +355,5 @@ async def test_check_requesterror(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown_error" diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py index 5992d480f80..9ad15096ad3 100644 --- a/tests/components/integration/test_config_flow.py +++ b/tests/components/integration/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.integration.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My integration" assert result["data"] == {} assert result["options"] == { @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -109,7 +109,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "round": 2.0, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "method": "left", "name": "My integration", diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 06fcbea5bfa..95e5d735c35 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING from homeassistant.components.intellifire.const import CONF_USER_ID, DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.intellifire.conftest import mock_api_connection_error @@ -38,7 +34,7 @@ async def test_no_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_device_entry" @@ -50,7 +46,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -59,7 +55,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Fireplace 12345" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -100,7 +96,7 @@ async def test_single_discovery( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["errors"] == {"base": "iftapi_connect"} @@ -133,7 +129,7 @@ async def test_single_discovery_loign_error( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["errors"] == {"base": "api_error"} @@ -199,14 +195,14 @@ async def test_multi_discovery_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_device" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -221,7 +217,7 @@ async def test_form_cannot_connect_manual_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_device_entry" result2 = await hass.config_entries.flow.async_configure( @@ -231,7 +227,7 @@ async def test_form_cannot_connect_manual_entry( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -266,7 +262,7 @@ async def test_picker_already_discovered( CONF_HOST: "192.168.1.4", }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert len(mock_setup_entry.mock_calls) == 0 @@ -303,7 +299,7 @@ async def test_reauth_flow( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -311,7 +307,7 @@ async def test_reauth_flow( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert entry.data[CONF_PASSWORD] == "AROONIE" assert entry.data[CONF_USERNAME] == "test" @@ -331,10 +327,10 @@ async def test_dhcp_discovery_intellifire_device( hostname="zentrios-Test", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "dhcp_confirm" result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "dhcp_confirm" result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={} diff --git a/tests/components/iotawatt/test_config_flow.py b/tests/components/iotawatt/test_config_flow.py index 6adb3534b15..06c9cce0da9 100644 --- a/tests/components/iotawatt/test_config_flow.py +++ b/tests/components/iotawatt/test_config_flow.py @@ -6,7 +6,7 @@ import httpx from homeassistant import config_entries from homeassistant.components.iotawatt.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -15,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == { "host": "1.1.1.1", } @@ -46,7 +46,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -59,7 +59,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "auth" with patch( @@ -75,7 +75,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["step_id"] == "auth" assert result3["errors"] == {"base": "invalid_auth"} @@ -95,7 +95,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 assert result4["data"] == { "host": "1.1.1.1", @@ -119,7 +119,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -138,5 +138,5 @@ async def test_form_setup_exception(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 8eefa4251f7..a37fdc9de52 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_USER_INPUT, @@ -31,7 +27,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_show_zeroconf_form( @@ -48,7 +44,7 @@ async def test_show_zeroconf_form( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} @@ -66,7 +62,7 @@ async def test_connection_error( ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -83,7 +79,7 @@ async def test_zeroconf_connection_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -98,7 +94,7 @@ async def test_zeroconf_confirm_connection_error( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -116,7 +112,7 @@ async def test_user_connection_upgrade_required( ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "connection_upgrade"} @@ -133,7 +129,7 @@ async def test_zeroconf_connection_upgrade_required( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "connection_upgrade" @@ -150,7 +146,7 @@ async def test_user_parse_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -167,7 +163,7 @@ async def test_zeroconf_parse_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -184,7 +180,7 @@ async def test_user_ipp_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -201,7 +197,7 @@ async def test_zeroconf_ipp_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -218,7 +214,7 @@ async def test_user_ipp_version_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -235,7 +231,7 @@ async def test_zeroconf_ipp_version_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -252,7 +248,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -269,7 +265,7 @@ async def test_zeroconf_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -291,7 +287,7 @@ async def test_zeroconf_with_uuid_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -312,7 +308,7 @@ async def test_zeroconf_empty_unique_id( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_zeroconf_no_unique_id( @@ -328,7 +324,7 @@ async def test_zeroconf_no_unique_id( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_full_user_flow_implementation( @@ -343,7 +339,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -351,7 +347,7 @@ async def test_full_user_flow_implementation( user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.31" assert result["data"] @@ -376,14 +372,14 @@ async def test_full_zeroconf_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -410,7 +406,7 @@ async def test_full_zeroconf_tls_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): @@ -418,7 +414,7 @@ async def test_full_zeroconf_tls_flow_implementation( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] diff --git a/tests/components/kaleidescape/test_config_flow.py b/tests/components/kaleidescape/test_config_flow.py index a2cf8091d02..8171ed0955b 100644 --- a/tests/components/kaleidescape/test_config_flow.py +++ b/tests/components/kaleidescape/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.kaleidescape.const import DOMAIN from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_HOST, MOCK_SSDP_DISCOVERY_INFO @@ -25,7 +21,7 @@ async def test_user_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -33,7 +29,7 @@ async def test_user_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -48,7 +44,7 @@ async def test_user_config_flow_bad_connect_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -63,7 +59,7 @@ async def test_user_config_flow_unsupported_device_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unsupported"} @@ -75,7 +71,7 @@ async def test_user_config_flow_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -87,7 +83,7 @@ async def test_ssdp_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( @@ -95,7 +91,7 @@ async def test_ssdp_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -111,7 +107,7 @@ async def test_ssdp_config_flow_bad_connect_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -126,5 +122,5 @@ async def test_ssdp_config_flow_unsupported_device_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unsupported" diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 8d5d57567dc..30b8aa537a6 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -41,11 +41,7 @@ from homeassistant.components.knx.const import ( ) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, - RESULT_TYPE_MENU, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -84,7 +80,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -94,7 +90,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" assert not result2["errors"] @@ -111,7 +107,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == CONF_KNX_ROUTING.capitalize() assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -136,7 +132,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: "show_advanced_options": True, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -146,7 +142,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" assert not result2["errors"] @@ -161,7 +157,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result_invalid_input["type"] == RESULT_TYPE_FORM + assert result_invalid_input["type"] == FlowResultType.FORM assert result_invalid_input["step_id"] == "routing" assert result_invalid_input["errors"] == { CONF_KNX_MCAST_GRP: "invalid_ip_address", @@ -184,7 +180,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == CONF_KNX_ROUTING.capitalize() assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -261,7 +257,7 @@ async def test_tunneling_setup( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -271,7 +267,7 @@ async def test_tunneling_setup( }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert not result2["errors"] @@ -284,7 +280,7 @@ async def test_tunneling_setup( user_input, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Tunneling @ 192.168.0.1" assert result3["data"] == config_entry_data @@ -303,7 +299,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: "show_advanced_options": True, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -313,7 +309,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert not result2["errors"] @@ -328,7 +324,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result_invalid_host["type"] == RESULT_TYPE_FORM + assert result_invalid_host["type"] == FlowResultType.FORM assert result_invalid_host["step_id"] == "manual_tunnel" assert result_invalid_host["errors"] == {CONF_HOST: "invalid_ip_address"} # invalid local ip address @@ -342,7 +338,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result_invalid_local["type"] == RESULT_TYPE_FORM + assert result_invalid_local["type"] == FlowResultType.FORM assert result_invalid_local["step_id"] == "manual_tunnel" assert result_invalid_local["errors"] == {CONF_KNX_LOCAL_IP: "invalid_ip_address"} @@ -361,7 +357,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Tunneling @ 192.168.0.2" assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -385,7 +381,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -395,7 +391,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) }, ) await hass.async_block_till_done() - assert tunnel_flow["type"] == RESULT_TYPE_FORM + assert tunnel_flow["type"] == FlowResultType.FORM assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] @@ -404,7 +400,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) {CONF_KNX_GATEWAY: str(gateway)}, ) await hass.async_block_till_done() - assert manual_tunnel["type"] == RESULT_TYPE_FORM + assert manual_tunnel["type"] == FlowResultType.FORM assert manual_tunnel["step_id"] == "manual_tunnel" with patch( @@ -420,7 +416,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) }, ) await hass.async_block_till_done() - assert manual_tunnel_flow["type"] == RESULT_TYPE_CREATE_ENTRY + assert manual_tunnel_flow["type"] == FlowResultType.CREATE_ENTRY assert manual_tunnel_flow["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, @@ -441,7 +437,7 @@ async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -451,7 +447,7 @@ async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert tunnel_flow["type"] == RESULT_TYPE_FORM + assert tunnel_flow["type"] == FlowResultType.FORM assert tunnel_flow["step_id"] == "manual_tunnel" assert not tunnel_flow["errors"] @@ -463,7 +459,7 @@ async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch( @@ -478,7 +474,7 @@ async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> N ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() assert result2["data"] == { **DEFAULT_ENTRY_DATA, @@ -496,7 +492,7 @@ async def _get_menu_step(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -506,7 +502,7 @@ async def _get_menu_step(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert not result2["errors"] @@ -519,7 +515,7 @@ async def _get_menu_step(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_MENU + assert result3["type"] == FlowResultType.MENU assert result3["step_id"] == "secure_tunneling" return result3 @@ -532,7 +528,7 @@ async def test_configure_secure_manual(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_manual"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_manual" assert not result["errors"] @@ -549,7 +545,7 @@ async def test_configure_secure_manual(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_manual["type"] == RESULT_TYPE_CREATE_ENTRY + assert secure_manual["type"] == FlowResultType.CREATE_ENTRY assert secure_manual["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -574,7 +570,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -592,7 +588,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == RESULT_TYPE_CREATE_ENTRY + assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY assert secure_knxkeys["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -616,7 +612,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -632,7 +628,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == RESULT_TYPE_FORM + assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_FILENAME] == "file_not_found" @@ -645,7 +641,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -661,7 +657,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == RESULT_TYPE_FORM + assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] == "invalid_signature" @@ -679,7 +675,7 @@ async def test_options_flow( mock_config_entry.entry_id ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -694,7 +690,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == { @@ -787,7 +783,7 @@ async def test_tunneling_options_flow( mock_config_entry.entry_id ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -801,7 +797,7 @@ async def test_tunneling_options_flow( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert not result2.get("data") assert "flow_id" in result2 @@ -811,7 +807,7 @@ async def test_tunneling_options_flow( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert not result3.get("data") assert mock_config_entry.data == config_entry_data @@ -880,7 +876,7 @@ async def test_advanced_options( mock_config_entry.entry_id, context={"show_advanced_options": True} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -890,7 +886,7 @@ async def test_advanced_options( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == config_entry_data diff --git a/tests/components/laundrify/test_config_flow.py b/tests/components/laundrify/test_config_flow.py index 5ee3efe1e45..d9715c2d025 100644 --- a/tests/components/laundrify/test_config_flow.py +++ b/tests/components/laundrify/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components.laundrify.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CODE, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import create_entry from .const import VALID_ACCESS_TOKEN, VALID_AUTH_CODE, VALID_USER_INPUT @@ -21,7 +17,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_ACCESS_TOKEN: VALID_ACCESS_TOKEN, @@ -50,7 +46,7 @@ async def test_form_invalid_format( data={CONF_CODE: "invalidFormat"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_format"} @@ -63,7 +59,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, laundrify_exchange_code) - data=VALID_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_auth"} @@ -76,7 +72,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, laundrify_exchange_code) data=VALID_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -89,7 +85,7 @@ async def test_form_unkown_exception(hass: HomeAssistant, laundrify_exchange_cod data=VALID_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -99,7 +95,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_REAUTH} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -108,7 +104,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_integration_already_exists(hass: HomeAssistant): @@ -125,5 +121,5 @@ async def test_integration_already_exists(hass: HomeAssistant): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py index e24b9c92221..daa35f31845 100644 --- a/tests/components/lookin/test_config_flow.py +++ b/tests/components/lookin/test_config_flow.py @@ -10,11 +10,7 @@ from homeassistant import config_entries from homeassistant.components.lookin.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_TITLE, @@ -45,7 +41,7 @@ async def test_manual_setup(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: IP_ADDRESS} assert result["title"] == DEFAULT_ENTRY_TITLE assert len(mock_setup_entry.mock_calls) == 1 @@ -70,7 +66,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -123,7 +119,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_get_info(), patch( @@ -132,7 +128,7 @@ async def test_discovered_zeroconf(hass): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == {CONF_HOST: IP_ADDRESS} assert result2["title"] == DEFAULT_ENTRY_TITLE assert mock_async_setup_entry.called @@ -151,7 +147,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "127.0.0.2" @@ -167,7 +163,7 @@ async def test_discovered_zeroconf_cannot_connect(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -182,5 +178,5 @@ async def test_discovered_zeroconf_unknown_exception(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 595d4397200..25d41c0c2d0 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.luftdaten.const import CONF_SENSOR_ID from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -36,7 +32,7 @@ async def test_duplicate_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -48,7 +44,7 @@ async def test_communication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -58,7 +54,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"} assert "flow_id" in result2 @@ -69,7 +65,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -85,7 +81,7 @@ async def test_invalid_sensor( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -95,7 +91,7 @@ async def test_invalid_sensor( user_input={CONF_SENSOR_ID: 11111}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"} assert "flow_id" in result2 @@ -106,7 +102,7 @@ async def test_invalid_sensor( user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -124,7 +120,7 @@ async def test_step_user( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -136,7 +132,7 @@ async def test_step_user( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SENSOR_ID: 12345, diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index ff2f7393c82..44fea69409b 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.mill.const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +15,7 @@ async def test_show_config_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -24,7 +24,7 @@ async def test_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -33,7 +33,7 @@ async def test_create_entry(hass): CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -72,7 +72,7 @@ async def test_flow_entry_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -81,7 +81,7 @@ async def test_flow_entry_already_exists(hass): CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -99,7 +99,7 @@ async def test_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -108,7 +108,7 @@ async def test_connection_error(hass): CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("mill.Mill.connect", return_value=False): result = await hass.config_entries.flow.async_configure( @@ -119,7 +119,7 @@ async def test_connection_error(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -128,7 +128,7 @@ async def test_local_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -137,7 +137,7 @@ async def test_local_create_entry(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -180,7 +180,7 @@ async def test_local_flow_entry_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -189,7 +189,7 @@ async def test_local_flow_entry_already_exists(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -219,7 +219,7 @@ async def test_local_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -228,7 +228,7 @@ async def test_local_connection_error(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -243,5 +243,5 @@ async def test_local_connection_error(hass): test_data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py index ac617eb938c..0eb334763d6 100644 --- a/tests/components/min_max/test_config_flow.py +++ b/tests/components/min_max/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.min_max.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -32,7 +32,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My min_max" assert result["data"] == {} assert result["options"] == { @@ -92,7 +92,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "entity_ids") == input_sensors1 @@ -107,7 +107,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "type": "mean", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_ids": input_sensors2, "name": "My min_max", diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 9717fa0052b..7d6b8227396 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.minecraft_server.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -86,7 +82,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -97,7 +93,7 @@ async def test_invalid_ip(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4 ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_ip"} @@ -122,7 +118,7 @@ async def test_same_host(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -136,7 +132,7 @@ async def test_port_too_small(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_SMALL ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_port"} @@ -150,7 +146,7 @@ async def test_port_too_large(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_LARGE ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_port"} @@ -164,7 +160,7 @@ async def test_connection_failed(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -178,7 +174,7 @@ async def test_connection_succeeded_with_srv_record(hass: HomeAssistant) -> None DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_SRV ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_SRV[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_SRV[CONF_NAME] assert result["data"][CONF_HOST] == USER_INPUT_SRV[CONF_HOST] @@ -194,7 +190,7 @@ async def test_connection_succeeded_with_host(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME] assert result["data"][CONF_HOST] == "mc.dummyserver.com" @@ -213,7 +209,7 @@ async def test_connection_succeeded_with_ip4(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4 ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_IPV4[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_IPV4[CONF_NAME] assert result["data"][CONF_HOST] == "1.1.1.1" @@ -232,7 +228,7 @@ async def test_connection_succeeded_with_ip6(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV6 ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_IPV6[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_IPV6[CONF_NAME] assert result["data"][CONF_HOST] == "::ffff:0101:0101" diff --git a/tests/components/mjpeg/test_config_flow.py b/tests/components/mjpeg/test_config_flow.py index cdf56ab97c0..3d66af21f0a 100644 --- a/tests/components/mjpeg/test_config_flow.py +++ b/tests/components/mjpeg/test_config_flow.py @@ -20,11 +20,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -39,7 +35,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -55,7 +51,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Spy cam" assert result2.get("data") == {} assert result2.get("options") == { @@ -85,7 +81,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -102,7 +98,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"username": "invalid_auth"} assert "flow_id" in result2 @@ -121,7 +117,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Sky cam" assert result3.get("data") == {} assert result3.get("options") == { @@ -147,7 +143,7 @@ async def test_connection_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -164,7 +160,7 @@ async def test_connection_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"mjpeg_url": "cannot_connect"} assert "flow_id" in result2 @@ -188,7 +184,7 @@ async def test_connection_error( }, ) - assert result3.get("type") == RESULT_TYPE_FORM + assert result3.get("type") == FlowResultType.FORM assert result3.get("step_id") == SOURCE_USER assert result3.get("errors") == {"still_image_url": "cannot_connect"} assert "flow_id" in result3 @@ -209,7 +205,7 @@ async def test_connection_error( }, ) - assert result4.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result4.get("type") == FlowResultType.CREATE_ENTRY assert result4.get("title") == "My cam" assert result4.get("data") == {} assert result4.get("options") == { @@ -247,7 +243,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -259,7 +255,7 @@ async def test_options_flow( """Test options config flow.""" result = await hass.config_entries.options.async_init(init_integration.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -288,7 +284,7 @@ async def test_options_flow( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "init" assert result2.get("errors") == {"mjpeg_url": "already_configured"} assert "flow_id" in result2 @@ -307,7 +303,7 @@ async def test_options_flow( }, ) - assert result3.get("type") == RESULT_TYPE_FORM + assert result3.get("type") == FlowResultType.FORM assert result3.get("step_id") == "init" assert result3.get("errors") == {"mjpeg_url": "cannot_connect"} assert "flow_id" in result3 @@ -326,7 +322,7 @@ async def test_options_flow( }, ) - assert result4.get("type") == RESULT_TYPE_FORM + assert result4.get("type") == FlowResultType.FORM assert result4.get("step_id") == "init" assert result4.get("errors") == {"still_image_url": "cannot_connect"} assert "flow_id" in result4 @@ -346,7 +342,7 @@ async def test_options_flow( }, ) - assert result5.get("type") == RESULT_TYPE_FORM + assert result5.get("type") == FlowResultType.FORM assert result5.get("step_id") == "init" assert result5.get("errors") == {"username": "invalid_auth"} assert "flow_id" in result5 @@ -363,7 +359,7 @@ async def test_options_flow( }, ) - assert result6.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result6.get("type") == FlowResultType.CREATE_ENTRY assert result6.get("data") == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_MJPEG_URL: "https://example.com/mjpeg", diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 931d2918fe2..05cebb2fef5 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.modern_forms.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import init_integration @@ -37,7 +33,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result with patch( @@ -50,7 +46,7 @@ async def test_full_user_flow_implementation( assert result2.get("title") == "ModernFormsFan" assert "data" in result2 - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2["data"][CONF_HOST] == "192.168.1.123" assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF" assert len(mock_setup_entry.mock_calls) == 1 @@ -85,7 +81,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {CONF_NAME: "example"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result flow = flows[0] @@ -98,7 +94,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result2.get("title") == "example" - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -121,7 +117,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -150,7 +146,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -182,7 +178,7 @@ async def test_zeroconf_confirm_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -218,7 +214,7 @@ async def test_user_device_exists_abort( }, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -252,5 +248,5 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/moehlenhoff_alpha2/test_config_flow.py b/tests/components/moehlenhoff_alpha2/test_config_flow.py index ccfa98718e5..a1f98454015 100644 --- a/tests/components/moehlenhoff_alpha2/test_config_flow.py +++ b/tests/components/moehlenhoff_alpha2/test_config_flow.py @@ -5,11 +5,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.moehlenhoff_alpha2.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -33,7 +29,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch("moehlenhoff_alpha2.Alpha2Base.update_data", mock_update_data), patch( @@ -46,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_BASE_NAME assert result2["data"] == {"host": MOCK_BASE_HOST} assert len(mock_setup_entry.mock_calls) == 1 @@ -70,7 +66,7 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: data={"host": MOCK_BASE_HOST}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -87,7 +83,7 @@ async def test_form_cannot_connect_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -102,5 +98,5 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py index 4bfb61166aa..9dfc186d492 100644 --- a/tests/components/moon/test_config_flow.py +++ b/tests/components/moon/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.moon.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +21,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -34,7 +30,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Moon" assert result2.get("data") == {} @@ -52,7 +48,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -67,6 +63,6 @@ async def test_import_flow( data={CONF_NAME: "My Moon"}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "My Moon" assert result.get("data") == {} diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index 967ad0dbcc4..451e9ec4b90 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -5,7 +5,7 @@ from mullvad_api import MullvadAPIError from homeassistant import config_entries, setup from homeassistant.components.mullvad.const import DOMAIN -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -16,7 +16,7 @@ async def test_form_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch( @@ -45,7 +45,7 @@ async def test_form_user_only_once(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -66,7 +66,7 @@ async def test_connection_error(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -87,5 +87,5 @@ async def test_unknown_error(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index 8799e3adcf0..fd28f6efa41 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -6,11 +6,7 @@ from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SCAN_INTERVAL, CONF_VERIFY_SSL -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( ENTRY_CONFIG, @@ -30,7 +26,7 @@ async def test_user_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry() as mock_setup_entry: @@ -40,7 +36,7 @@ async def test_user_form(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: False} @@ -53,7 +49,7 @@ async def test_user_form_show_advanced_options(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} user_input_advanced = { @@ -68,7 +64,7 @@ async def test_user_form_show_advanced_options(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: True} @@ -90,7 +86,7 @@ async def test_user_form_cannot_connect(hass): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -109,7 +105,7 @@ async def test_user_form_unexpected_exception(hass): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -123,7 +119,7 @@ async def test_user_form_single_instance_allowed(hass): context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -143,7 +139,7 @@ async def test_options_flow(hass, nzbget_api): assert entry.options[CONF_SCAN_INTERVAL] == 5 result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with _patch_async_setup_entry(): @@ -153,5 +149,5 @@ async def test_options_flow(hass, nzbget_api): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 15 diff --git a/tests/components/oncue/test_config_flow.py b/tests/components/oncue/test_config_flow.py index df9de02a6b3..718a3b08adb 100644 --- a/tests/components/oncue/test_config_flow.py +++ b/tests/components/oncue/test_config_flow.py @@ -7,11 +7,7 @@ from aiooncue import LoginFailedException from homeassistant import config_entries from homeassistant.components.oncue.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch("homeassistant.components.oncue.config_flow.Oncue.async_login"), patch( @@ -37,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "TEST-username", @@ -64,7 +60,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -86,7 +82,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -108,7 +104,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -137,5 +133,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index d599c10ea90..57677fc5bff 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.onewire.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture(autouse=True, name="mock_setup_entry") @@ -29,7 +25,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] # Invalid server @@ -42,7 +38,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -55,7 +51,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "1.2.3.4" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -76,7 +72,7 @@ async def test_user_duplicate( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -85,7 +81,7 @@ async def test_user_duplicate( result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/onewire/test_options_flow.py b/tests/components/onewire/test_options_flow.py index e27b5a368d9..795f8a50c99 100644 --- a/tests/components/onewire/test_options_flow.py +++ b/tests/components/onewire/test_options_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.onewire.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import setup_owproxy_mock_devices from .const import MOCK_OWPROXY_DEVICES @@ -49,7 +45,7 @@ async def test_user_options_clear( result["flow_id"], user_input={INPUT_ENTRY_CLEAR_OPTIONS: True}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {} @@ -78,7 +74,7 @@ async def test_user_options_empty_selection( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: []}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "device_selection" assert result["errors"] == {"base": "device_not_selected"} @@ -111,7 +107,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: ["28.111111111111"]}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"]["sensor_id"] == "28.111111111111" # Verify that the setting for the device comes back as default when no input is given @@ -119,7 +115,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.111111111111"]["precision"] == "temperature" @@ -167,7 +163,7 @@ async def test_user_options_set_multiple( ] }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.222222222222)" @@ -178,7 +174,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.111111111111)" @@ -189,7 +185,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature9"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.222222222222"]["precision"] == "temperature" @@ -213,5 +209,5 @@ async def test_user_options_no_devices( # Verify that first config step comes back with an empty list of possible devices to choose from result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "No configurable devices found." diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py index f985e2a6193..0dd81d35856 100644 --- a/tests/components/open_meteo/test_config_flow.py +++ b/tests/components/open_meteo/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components.zone import ENTITY_ID_HOME from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_ZONE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow( @@ -19,7 +19,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -28,6 +28,6 @@ async def test_full_user_flow( user_input={CONF_ZONE: ENTITY_ID_HOME}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "test home" assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME} diff --git a/tests/components/opengarage/test_config_flow.py b/tests/components/opengarage/test_config_flow.py index 5406c40b3aa..39cbb9c4b6b 100644 --- a/tests/components/opengarage/test_config_flow.py +++ b/tests/components/opengarage/test_config_flow.py @@ -6,11 +6,7 @@ import aiohttp from homeassistant import config_entries from homeassistant.components.opengarage.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -37,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "http://1.1.1.1", @@ -63,7 +59,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -82,7 +78,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -101,7 +97,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -132,5 +128,5 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index f6ce5fe5d9d..42a41789a92 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components.p1_monitor.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow(hass: HomeAssistant) -> None: @@ -16,7 +16,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +33,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_HOST: "example.com", @@ -58,5 +58,5 @@ async def test_api_error(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py index 98f704e3af8..afa158e2dd9 100644 --- a/tests/components/peco/test_config_flow.py +++ b/tests/components/peco/test_config_flow.py @@ -7,7 +7,7 @@ from voluptuous.error import MultipleInvalid from homeassistant import config_entries from homeassistant.components.peco.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -15,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Philadelphia Outage Count" assert result2["data"] == { "county": "PHILADELPHIA", @@ -42,7 +42,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with raises(MultipleInvalid): @@ -57,7 +57,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: second_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert second_result["type"] == RESULT_TYPE_FORM + assert second_result["type"] == FlowResultType.FORM assert second_result["errors"] is None with patch( @@ -72,7 +72,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert second_result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert second_result2["type"] == FlowResultType.CREATE_ENTRY assert second_result2["title"] == "Philadelphia Outage Count" assert second_result2["data"] == { "county": "PHILADELPHIA", diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 517697b0e8a..bc86922c89f 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -5,11 +5,7 @@ from unittest.mock import patch from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_API_KEY -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_CONFIG_ENTRY, @@ -44,7 +40,7 @@ async def test_flow_import(hass, caplog): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_CONFIG_ENTRY @@ -52,7 +48,7 @@ async def test_flow_import(hass, caplog): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -63,7 +59,7 @@ async def test_flow_import_invalid(hass, caplog): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len([x for x in caplog.records if x.levelno == logging.ERROR]) == 1 @@ -76,7 +72,7 @@ async def test_flow_user(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} _flow_next(hass, result["flow_id"]) @@ -85,7 +81,7 @@ async def test_flow_user(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW_USER, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_key" assert result["errors"] is None _flow_next(hass, result["flow_id"]) @@ -94,7 +90,7 @@ async def test_flow_user(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW_API_KEY, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_CONFIG_ENTRY @@ -104,7 +100,7 @@ async def test_flow_user(hass): context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -116,7 +112,7 @@ async def test_flow_statistics_only(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} _flow_next(hass, result["flow_id"]) @@ -130,7 +126,7 @@ async def test_flow_statistics_only(hass): result["flow_id"], user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == config_entry_data @@ -142,6 +138,6 @@ async def test_flow_user_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index b7b39aa50b4..f3abd5a6905 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -12,11 +12,7 @@ from homeassistant.components.plaato.const import ( DOMAIN, ) from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -59,7 +55,7 @@ async def test_show_config_form_device_type_airlock(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) == bool @@ -73,7 +69,7 @@ async def test_show_config_form_device_type_keg(hass): data={CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, CONF_DEVICE_NAME: "device_name"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is None @@ -86,7 +82,7 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -97,7 +93,7 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -119,7 +115,7 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "webhook" @@ -130,7 +126,7 @@ async def test_show_config_form_validate_webhook_not_connected(hass, webhook_id) DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -141,7 +137,7 @@ async def test_show_config_form_validate_webhook_not_connected(hass, webhook_id) }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -163,7 +159,7 @@ async def test_show_config_form_validate_webhook_not_connected(hass, webhook_id) }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" @@ -182,7 +178,7 @@ async def test_show_config_form_validate_token(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): @@ -190,7 +186,7 @@ async def test_show_config_form_validate_token(hass): result["flow_id"], user_input={CONF_TOKEN: "valid_token"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == PlaatoDeviceType.Keg.name assert result["data"] == { CONF_USE_WEBHOOK: False, @@ -215,7 +211,7 @@ async def test_show_config_form_no_cloud_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( @@ -226,7 +222,7 @@ async def test_show_config_form_no_cloud_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "webhook" assert result["errors"] is None @@ -247,14 +243,14 @@ async def test_show_config_form_api_method_no_auth_token(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_auth_token" @@ -272,14 +268,14 @@ async def test_show_config_form_api_method_no_auth_token(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_api_method" diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 3ef9e9c0fd1..f4dcfd87b8b 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +from homeassistant.data_entry_flow import FlowResultType from .mocks import ( MOCK_GATEWAY_DIN, @@ -299,7 +299,7 @@ async def test_dhcp_discovery_cannot_connect(hass): hostname="00GGX", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -370,7 +370,7 @@ async def test_dhcp_discovery_update_ip_address(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1" @@ -402,7 +402,7 @@ async def test_dhcp_discovery_updates_unique_id(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" assert entry.unique_id == MOCK_GATEWAY_DIN diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 8c850e0807a..cef6b87963a 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.progettihwsw.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -26,7 +22,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -43,7 +39,7 @@ async def test_form(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "relay_modes" assert result2["errors"] == {} @@ -56,7 +52,7 @@ async def test_form(hass): mock_value_step_rm, ) - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["data"] assert result3["data"]["title"] == "1R & 1IN Board" assert result3["data"]["is_old"] is False @@ -80,7 +76,7 @@ async def test_form_cannot_connect(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -107,7 +103,7 @@ async def test_form_existing_entry_exception(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -128,6 +124,6 @@ async def test_form_user_exception(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index 46832146cf8..8a608d4eeba 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -6,7 +6,7 @@ from pytest import mark from homeassistant import config_entries from homeassistant.components.prosegur.config_flow import CannotConnect, InvalidAuth from homeassistant.components.prosegur.const import DOMAIN -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -145,7 +145,7 @@ async def test_reauth_flow(hass): data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} install = MagicMock() @@ -167,7 +167,7 @@ async def test_reauth_flow(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "country": "PT", @@ -223,5 +223,5 @@ async def test_reauth_flow_error(hass, exception, base_error): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == base_error diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py index 441a5977a2d..d1ed8eeb578 100644 --- a/tests/components/pure_energie/test_config_flow.py +++ b/tests/components/pure_energie/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.pure_energie.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow_implementation( @@ -27,7 +23,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == SOURCE_USER - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result = await hass.config_entries.flow.async_configure( @@ -35,7 +31,7 @@ async def test_full_user_flow_implementation( ) assert result.get("title") == "Pure Energie Meter" - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -67,7 +63,7 @@ async def test_full_zeroconf_flow_implementationn( CONF_NAME: "Pure Energie Meter", } assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( @@ -75,7 +71,7 @@ async def test_full_zeroconf_flow_implementationn( ) assert result2.get("title") == "Pure Energie Meter" - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -94,7 +90,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -119,5 +115,5 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 444a35565f6..9d6162e4d46 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -39,7 +35,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SYSTEM_ID: 12345, @@ -64,7 +60,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -77,7 +73,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -94,7 +90,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SYSTEM_ID: 12345, @@ -120,7 +116,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 @@ -147,7 +143,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -169,7 +165,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -179,7 +175,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -212,7 +208,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -223,7 +219,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -238,7 +234,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("type") == FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -266,7 +262,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -277,6 +273,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/radio_browser/test_config_flow.py b/tests/components/radio_browser/test_config_flow.py index 8a5a3d9ccce..56ed98f145b 100644 --- a/tests/components/radio_browser/test_config_flow.py +++ b/tests/components/radio_browser/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import AsyncMock from homeassistant.components.radio_browser.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +14,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") is None assert "flow_id" in result @@ -27,7 +23,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Radio Browser" assert result2.get("data") == {} @@ -46,7 +42,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -58,7 +54,7 @@ async def test_onboarding_flow( DOMAIN, context={"source": "onboarding"} ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "Radio Browser" assert result.get("data") == {} diff --git a/tests/components/rainforest_eagle/test_config_flow.py b/tests/components/rainforest_eagle/test_config_flow.py index 370dfebee90..d9b66b0feec 100644 --- a/tests/components/rainforest_eagle/test_config_flow.py +++ b/tests/components/rainforest_eagle/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components.rainforest_eagle.const import ( from homeassistant.components.rainforest_eagle.data import CannotConnect, InvalidAuth from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -21,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "abcdef" assert result2["data"] == { CONF_TYPE: TYPE_EAGLE_200, @@ -72,7 +72,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -95,5 +95,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/raspberry_pi/test_config_flow.py b/tests/components/raspberry_pi/test_config_flow.py index dfad1100cad..68306b3ea9a 100644 --- a/tests/components/raspberry_pi/test_config_flow.py +++ b/tests/components/raspberry_pi/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.raspberry_pi.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Raspberry Pi" assert result["data"] == {} assert result["options"] == {} @@ -53,6 +53,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/rdw/test_config_flow.py b/tests/components/rdw/test_config_flow.py index 20144768abe..0fe40c29dfa 100644 --- a/tests/components/rdw/test_config_flow.py +++ b/tests/components/rdw/test_config_flow.py @@ -7,7 +7,7 @@ from vehicle.exceptions import RDWConnectionError, RDWUnknownLicensePlateError from homeassistant.components.rdw.const import CONF_LICENSE_PLATE, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow( @@ -18,7 +18,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -29,7 +29,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "11-ZKZ-3" assert result2.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -46,7 +46,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -58,7 +58,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "unknown_license_plate"} assert "flow_id" in result2 @@ -71,7 +71,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "11-ZKZ-3" assert result3.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -88,5 +88,5 @@ async def test_connection_error( data={CONF_LICENSE_PLATE: "0001TJ"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py index 358ac6783ad..a28660bb7a4 100644 --- a/tests/components/ridwell/test_config_flow.py +++ b/tests/components/ridwell/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.components.ridwell.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_duplicate_error(hass: HomeAssistant, config, config_entry): @@ -20,7 +16,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -39,7 +35,7 @@ async def test_errors(hass: HomeAssistant, config, error, exc) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"]["base"] == error @@ -48,7 +44,7 @@ async def test_show_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -66,7 +62,7 @@ async def test_step_reauth( result["flow_id"], user_input={CONF_PASSWORD: "password"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -76,4 +72,4 @@ async def test_step_user(hass: HomeAssistant, config, setup_ridwell) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index f5a3d270f70..bac6d7456a3 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.roku import ( @@ -39,7 +35,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" user_input = {CONF_HOST: mock_config_entry.data[CONF_HOST]} @@ -47,7 +43,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) @@ -55,7 +51,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -68,7 +64,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} user_input = {CONF_HOST: HOST} @@ -77,7 +73,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My Roku 3" assert "data" in result @@ -101,7 +97,7 @@ async def test_form_cannot_connect( flow_id=result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -120,7 +116,7 @@ async def test_form_unknown_error( flow_id=result["flow_id"], user_input=user_input ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -137,7 +133,7 @@ async def test_homekit_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -154,7 +150,7 @@ async def test_homekit_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -170,7 +166,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV} @@ -179,7 +175,7 @@ async def test_homekit_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME_ROKUTV assert "data" in result @@ -192,7 +188,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -209,7 +205,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -226,7 +222,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -241,7 +237,7 @@ async def test_ssdp_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} @@ -250,7 +246,7 @@ async def test_ssdp_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == UPNP_FRIENDLY_NAME assert result["data"] diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py index 7e302b51512..5c474fc0821 100644 --- a/tests/components/rpi_power/test_config_flow.py +++ b/tests/components/rpi_power/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import MagicMock from homeassistant.components.rpi_power.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import patch @@ -21,13 +17,13 @@ async def test_setup(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert not result["errors"] with patch(MODULE, return_value=MagicMock()): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY async def test_not_supported(hass: HomeAssistant) -> None: @@ -39,7 +35,7 @@ async def test_not_supported(hass: HomeAssistant) -> None: with patch(MODULE, return_value=None): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -50,7 +46,7 @@ async def test_onboarding(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY async def test_onboarding_not_supported(hass: HomeAssistant) -> None: @@ -60,5 +56,5 @@ async def test_onboarding_not_supported(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 1444d82cc5b..2c5e9e1ffc9 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_SSL, CONF_URL, ) -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType VALID_CONFIG = { CONF_NAME: "Sabnzbd", @@ -39,7 +39,7 @@ async def test_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 11aaf12d9ee..0b49a064a19 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -57,11 +57,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry @@ -363,7 +359,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -375,7 +371,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT @@ -447,7 +443,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} with patch( @@ -459,7 +455,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -536,7 +532,7 @@ async def test_ssdp_legacy_not_remote_control_receiver_udn( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -582,7 +578,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -591,7 +587,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_model" @@ -611,7 +607,7 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -743,7 +739,7 @@ async def test_ssdp_encrypted_websocket_not_supported( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -1261,7 +1257,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -1276,7 +1272,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: {}, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT diff --git a/tests/components/season/test_config_flow.py b/tests/components/season/test_config_flow.py index 11ebea8f6d6..9a64bcd140a 100644 --- a/tests/components/season/test_config_flow.py +++ b/tests/components/season/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.season.const import ( from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +25,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -38,7 +34,7 @@ async def test_full_user_flow( user_input={CONF_TYPE: TYPE_ASTRONOMICAL}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Season" assert result2.get("data") == {CONF_TYPE: TYPE_ASTRONOMICAL} @@ -56,7 +52,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source}, data={CONF_TYPE: TYPE_ASTRONOMICAL} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -71,6 +67,6 @@ async def test_import_flow( data={CONF_NAME: "My Seasons", CONF_TYPE: TYPE_METEOROLOGICAL}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "My Seasons" assert result.get("data") == {CONF_TYPE: TYPE_METEOROLOGICAL} diff --git a/tests/components/senseme/test_config_flow.py b/tests/components/senseme/test_config_flow.py index e85845dcace..63850dff3b5 100644 --- a/tests/components/senseme/test_config_flow.py +++ b/tests/components/senseme/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components import dhcp from homeassistant.components.senseme.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_ADDRESS, @@ -42,7 +38,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -53,7 +49,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Haiku Fan" assert result2["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -68,7 +64,7 @@ async def test_form_user_manual_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -79,7 +75,7 @@ async def test_form_user_manual_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual" with patch( @@ -97,7 +93,7 @@ async def test_form_user_manual_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Haiku Fan" assert result3["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -118,7 +114,7 @@ async def test_form_user_no_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -129,7 +125,7 @@ async def test_form_user_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual" assert result2["errors"] == {CONF_HOST: "invalid_host"} @@ -141,7 +137,7 @@ async def test_form_user_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Haiku Fan" assert result3["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -156,7 +152,7 @@ async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -167,7 +163,7 @@ async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual" with patch( @@ -182,7 +178,7 @@ async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["step_id"] == "manual" assert result3["errors"] == {CONF_HOST: "cannot_connect"} @@ -214,7 +210,7 @@ async def test_discovery(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: MOCK_UUID}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -225,7 +221,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Haiku Fan" assert result2["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -257,7 +253,7 @@ async def test_discovery_existing_device_no_ip_change(hass: HomeAssistant) -> No context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: MOCK_UUID}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -286,7 +282,7 @@ async def test_discovery_existing_device_ip_change(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["info"]["address"] == "127.0.0.8" @@ -304,7 +300,7 @@ async def test_dhcp_discovery_existing_config_entry(hass: HomeAssistant) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -320,7 +316,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -331,7 +327,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Haiku Fan" assert result2["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -348,7 +344,7 @@ async def test_dhcp_discovery_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -361,5 +357,5 @@ async def test_dhcp_discovery_cannot_connect_no_uuid(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index 7e92e1f2eb3..509fe8633d7 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -12,11 +12,7 @@ import pytest from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -51,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["version"] == 2 assert result2["data"] == { "api_key": "1234567890", @@ -78,7 +74,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -111,7 +107,7 @@ async def test_flow_fails( }, ) - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Sensibo" assert result3["data"] == { "api_key": "1234567891", @@ -125,7 +121,7 @@ async def test_flow_get_no_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -152,7 +148,7 @@ async def test_flow_get_no_username(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -192,7 +188,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -211,7 +207,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -261,7 +257,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -280,7 +276,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -330,7 +326,7 @@ async def test_flow_reauth_no_username_or_device( data=entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -349,5 +345,5 @@ async def test_flow_reauth_no_username_or_device( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 77dd440a2da..984c486c69e 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -17,11 +17,7 @@ from homeassistant.components.sentry.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -32,7 +28,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert "flow_id" in result @@ -62,7 +58,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -82,7 +78,7 @@ async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": "bad_dsn"} @@ -102,7 +98,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": "unknown"} @@ -120,7 +116,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -138,7 +134,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("data") == { CONF_ENVIRONMENT: "Test", CONF_EVENT_CUSTOM_COMPONENTS: True, diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index 0171a522e50..cd2b5053ac7 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -6,11 +6,7 @@ from aioskybell import exceptions from homeassistant.components.skybell.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import CONF_CONFIG_FLOW, _patch_skybell, _patch_skybell_devices @@ -38,7 +34,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -46,7 +42,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_CONFIG_FLOW @@ -64,7 +60,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -75,7 +71,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -88,7 +84,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -100,7 +96,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -115,7 +111,7 @@ async def test_flow_import(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_CONFIG_FLOW @@ -133,5 +129,5 @@ async def test_flow_import_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_IMPORT}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/slimproto/test_config_flow.py b/tests/components/slimproto/test_config_flow.py index 0c0c843f0b5..15ea5434fc5 100644 --- a/tests/components/slimproto/test_config_flow.py +++ b/tests/components/slimproto/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock from homeassistant.components.slimproto.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == DEFAULT_NAME assert result.get("data") == {} @@ -34,5 +34,5 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index 8cf22b3634e..eeaa0d75f07 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -9,11 +9,7 @@ from pysma.exceptions import ( from homeassistant.components.sma.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_DEVICE, MOCK_USER_INPUT, _patch_async_setup_entry @@ -24,7 +20,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch("pysma.SMA.new_session", return_value=True), patch( @@ -36,7 +32,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_USER_INPUT["host"] assert result["data"] == MOCK_USER_INPUT @@ -57,7 +53,7 @@ async def test_form_cannot_connect(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -76,7 +72,7 @@ async def test_form_invalid_auth(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} assert len(mock_setup_entry.mock_calls) == 0 @@ -95,7 +91,7 @@ async def test_form_cannot_retrieve_device_info(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve_device_info"} assert len(mock_setup_entry.mock_calls) == 0 @@ -114,7 +110,7 @@ async def test_form_unexpected_exception(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} assert len(mock_setup_entry.mock_calls) == 0 @@ -138,6 +134,6 @@ async def test_form_already_configured(hass, mock_config_entry): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 59783995d23..5eea0974dee 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -13,11 +13,7 @@ from homeassistant.components.sonarr.const import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.sonarr import MOCK_REAUTH_INPUT, MOCK_USER_INPUT @@ -31,7 +27,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_cannot_connect( @@ -47,7 +43,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -67,7 +63,7 @@ async def test_invalid_auth( data=user_input, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -85,7 +81,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -108,14 +104,14 @@ async def test_full_reauth_flow_implementation( data=entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_REAUTH_INPUT.copy() @@ -124,7 +120,7 @@ async def test_full_reauth_flow_implementation( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "test-api-key-reauth" @@ -141,7 +137,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -151,7 +147,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -168,7 +164,7 @@ async def test_full_user_flow_advanced_options( DOMAIN, context={CONF_SOURCE: SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -181,7 +177,7 @@ async def test_full_user_flow_advanced_options( user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -203,7 +199,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -212,6 +208,6 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_UPCOMING_DAYS] == 2 assert result["data"][CONF_WANTED_MAX_ITEMS] == 100 diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 4d58639a1d0..85293d37d9c 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components import ssdp from homeassistant.components.songpal.const import CONF_ENDPOINT, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_DATA, @@ -79,7 +75,7 @@ async def test_flow_ssdp(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -93,7 +89,7 @@ async def test_flow_user(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None _flow_next(hass, result["flow_id"]) @@ -102,7 +98,7 @@ async def test_flow_user(hass): result["flow_id"], user_input={CONF_ENDPOINT: ENDPOINT}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == { CONF_NAME: MODEL, @@ -121,7 +117,7 @@ async def test_flow_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -137,7 +133,7 @@ async def test_flow_import_without_name(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_ENDPOINT: ENDPOINT} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == {CONF_NAME: MODEL, CONF_ENDPOINT: ENDPOINT} @@ -165,7 +161,7 @@ async def test_ssdp_bravia(hass): context={"source": SOURCE_SSDP}, data=ssdp_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_songpal_device" @@ -177,7 +173,7 @@ async def test_sddp_exist(hass): context={"source": SOURCE_SSDP}, data=SSDP_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -190,7 +186,7 @@ async def test_user_exist(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -206,7 +202,7 @@ async def test_import_exist(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -222,7 +218,7 @@ async def test_user_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -239,7 +235,7 @@ async def test_import_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" mocked_device.get_supported_methods.assert_called_once() diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 47957ead98e..7c3571f8f19 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -8,11 +8,7 @@ from sqlalchemy.exc import SQLAlchemyError from homeassistant import config_entries from homeassistant.components.sql.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( ENTRY_CONFIG, @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -43,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "db_url": "sqlite://", @@ -70,7 +66,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "db_url": "sqlite://", @@ -102,7 +98,7 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -112,7 +108,7 @@ async def test_flow_fails_db_url(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -133,7 +129,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER result5 = await hass.config_entries.flow.async_configure( @@ -141,7 +137,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: user_input=ENTRY_CONFIG_INVALID_QUERY, ) - assert result5["type"] == RESULT_TYPE_FORM + assert result5["type"] == FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -151,7 +147,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: user_input=ENTRY_CONFIG_NO_RESULTS, ) - assert result5["type"] == RESULT_TYPE_FORM + assert result5["type"] == FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -161,7 +157,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: user_input=ENTRY_CONFIG, ) - assert result5["type"] == RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == FlowResultType.CREATE_ENTRY assert result5["title"] == "Get Value" assert result5["options"] == { "db_url": "sqlite://", @@ -198,7 +194,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -211,7 +207,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "db_url": "sqlite://", @@ -243,7 +239,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -262,7 +258,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value Title", "db_url": "sqlite://", @@ -347,7 +343,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_OPT, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == { "query": "query_invalid", } @@ -362,7 +358,7 @@ async def test_options_flow_fails_invalid_query( }, ) - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["data"] == { "name": "Get Value", "value_template": None, diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 22181d73fd3..a36abcc77aa 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.squeezebox.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -50,7 +46,7 @@ async def test_user_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" assert CONF_HOST in result["data_schema"].schema for key in result["data_schema"].schema: @@ -62,7 +58,7 @@ async def test_user_form(hass): result["flow_id"], {CONF_HOST: HOST, CONF_PORT: PORT, CONF_USERNAME: "", CONF_PASSWORD: ""}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == { CONF_HOST: HOST, @@ -84,14 +80,14 @@ async def test_user_form_timeout(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} # simulate manual input of host result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: HOST2} ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "edit" assert CONF_HOST in result2["data_schema"].schema for key in result2["data_schema"].schema: @@ -113,7 +109,7 @@ async def test_user_form_duplicate(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} @@ -138,7 +134,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -162,7 +158,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -177,7 +173,7 @@ async def test_discovery(hass): context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" @@ -189,7 +185,7 @@ async def test_discovery_no_uuid(hass): context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" @@ -207,7 +203,7 @@ async def test_dhcp_discovery(hass): hostname="any", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" @@ -226,7 +222,7 @@ async def test_dhcp_discovery_no_server_found(hass): hostname="any", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -245,4 +241,4 @@ async def test_dhcp_discovery_existing_player(hass): hostname="any", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index ed887bb6049..0472b847e4e 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components import dhcp from homeassistant.components.steamist.const import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_DATA, @@ -46,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(no_device=True), patch( @@ -63,7 +59,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "127.0.0.1" assert result2["data"] == { "host": "127.0.0.1", @@ -76,7 +72,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(), patch( @@ -93,7 +89,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == DEVICE_NAME assert result2["data"] == DEFAULT_ENTRY_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -116,7 +112,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -137,7 +133,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -148,13 +144,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -162,7 +158,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -183,7 +179,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == DEVICE_NAME assert result3["data"] == DEFAULT_ENTRY_DATA mock_setup.assert_called_once() @@ -193,7 +189,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -201,7 +197,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -215,7 +211,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DISCOVERY_30303, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -225,7 +221,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -239,7 +235,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -254,7 +250,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), patch( @@ -265,7 +261,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -282,7 +278,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), patch( @@ -293,7 +289,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -312,7 +308,7 @@ async def test_discovered_by_dhcp_discovery_fails(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -331,7 +327,7 @@ async def test_discovered_by_dhcp_discovery_finds_non_steamist_device( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_steamist_device" @@ -359,7 +355,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == FORMATTED_MAC_ADDRESS @@ -393,7 +389,7 @@ async def test_discovered_by_dhcp_or_discovery_existing_unique_id_does_not_reloa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert not mock_setup.called assert not mock_setup_entry.called diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py index 6b5dd6fd4ce..50cd56341d6 100644 --- a/tests/components/stookalert/test_config_flow.py +++ b/tests/components/stookalert/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import patch from homeassistant.components.stookalert.const import CONF_PROVINCE, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Overijssel" assert result2.get("data") == { CONF_PROVINCE: "Overijssel", @@ -61,5 +57,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" diff --git a/tests/components/sun/test_config_flow.py b/tests/components/sun/test_config_flow.py index 1712e8f6fc9..7d20a57ba27 100644 --- a/tests/components/sun/test_config_flow.py +++ b/tests/components/sun/test_config_flow.py @@ -6,11 +6,7 @@ import pytest from homeassistant.components.sun.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +17,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -34,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} @@ -55,7 +51,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -69,7 +65,7 @@ async def test_import_flow( data={}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index e504a807f08..5f6aa6311e0 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -6,11 +6,7 @@ from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError from homeassistant import config_entries from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -26,7 +22,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -42,7 +38,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Sure Petcare" assert result2["data"] == { "username": "test-username", @@ -70,7 +66,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -92,7 +88,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -114,7 +110,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -145,7 +141,7 @@ async def test_flow_entry_already_exists( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index d80f7e24bb1..8664e3e0379 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -33,7 +33,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -45,7 +45,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { @@ -88,7 +88,7 @@ async def test_config_flow_registered_entity( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -100,7 +100,7 @@ async def test_config_flow_registered_entity( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 59871681dfe..814e79cf591 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.switchbot.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import USER_INPUT, USER_INPUT_CURTAIN, init_integration, patch_async_setup_entry @@ -26,7 +22,7 @@ async def test_user_form_valid_mac(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -37,7 +33,7 @@ async def test_user_form_valid_mac(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { CONF_MAC: "e7:89:43:99:99:99", @@ -53,7 +49,7 @@ async def test_user_form_valid_mac(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -64,7 +60,7 @@ async def test_user_form_valid_mac(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { CONF_MAC: "e7:89:43:90:90:90", @@ -80,7 +76,7 @@ async def test_user_form_valid_mac(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_unconfigured_devices" @@ -93,7 +89,7 @@ async def test_user_form_exception(hass, switchbot_config_flow): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" switchbot_config_flow.side_effect = Exception @@ -102,7 +98,7 @@ async def test_user_form_exception(hass, switchbot_config_flow): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -112,7 +108,7 @@ async def test_options_flow(hass): entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -127,7 +123,7 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 60 assert result["data"][CONF_RETRY_COUNT] == 3 assert result["data"][CONF_RETRY_TIMEOUT] == 5 @@ -141,7 +137,7 @@ async def test_options_flow(hass): entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -156,7 +152,7 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 66 assert result["data"][CONF_RETRY_COUNT] == 6 assert result["data"][CONF_RETRY_TIMEOUT] == 6 diff --git a/tests/components/switcher_kis/test_config_flow.py b/tests/components/switcher_kis/test_config_flow.py index 2029e4a8ef3..f5ad68b6033 100644 --- a/tests/components/switcher_kis/test_config_flow.py +++ b/tests/components/switcher_kis/test_config_flow.py @@ -5,11 +5,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.switcher_kis.const import DATA_DISCOVERY, DOMAIN -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE @@ -25,7 +21,7 @@ async def test_import(hass): DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Switcher" assert result["data"] == {} @@ -53,7 +49,7 @@ async def test_user_setup(hass, mock_bridge): assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 2 - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -62,7 +58,7 @@ async def test_user_setup(hass, mock_bridge): ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Switcher" assert result2["result"].data == {} @@ -78,13 +74,13 @@ async def test_user_setup_abort_no_devices_found(hass, mock_bridge): assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 0 - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -104,5 +100,5 @@ async def test_single_instance(hass, source): DOMAIN, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/tailscale/test_config_flow.py b/tests/components/tailscale/test_config_flow.py index eb070cfdbb2..78e3f20a61b 100644 --- a/tests/components/tailscale/test_config_flow.py +++ b/tests/components/tailscale/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.tailscale.const import CONF_TAILNET, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -39,7 +35,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "homeassistant.github" assert result2.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -64,7 +60,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -77,7 +73,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -94,7 +90,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "homeassistant.github" assert result3.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -120,7 +116,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 @@ -144,7 +140,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -154,7 +150,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -187,7 +183,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -198,7 +194,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -213,7 +209,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("type") == FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -241,7 +237,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -252,6 +248,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index cae78a447f8..600bfd98c73 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -19,11 +19,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -91,7 +87,7 @@ async def test_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -103,13 +99,13 @@ async def test_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_STATIONS_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_NAME] == "Home" assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx" assert result["data"][CONF_FUEL_TYPES] == ["e5"] @@ -139,14 +135,14 @@ async def test_user_already_configured(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +151,7 @@ async def test_exception_security(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -166,7 +162,7 @@ async def test_exception_security(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_API_KEY] == "invalid_auth" @@ -176,7 +172,7 @@ async def test_user_no_stations(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -186,7 +182,7 @@ async def test_user_no_stations(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_RADIUS] == "no_stations" @@ -202,8 +198,8 @@ async def test_import(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_NAME] == "Home" assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx" assert result["data"][CONF_FUEL_TYPES] == ["e5"] @@ -243,7 +239,7 @@ async def test_reauth(hass: HomeAssistant): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # re-auth unsuccessful @@ -254,7 +250,7 @@ async def test_reauth(hass: HomeAssistant): CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_API_KEY: "invalid_auth"} @@ -266,7 +262,7 @@ async def test_reauth(hass: HomeAssistant): CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" mock_setup_entry.assert_called() @@ -297,7 +293,7 @@ async def test_options_flow(hass: HomeAssistant): assert mock_setup_entry.called result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -306,5 +302,5 @@ async def test_options_flow(hass: HomeAssistant): CONF_STATIONS: MOCK_OPTIONS_DATA[CONF_STATIONS], }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index 2e286f75b61..5c4ba11b05a 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components import dhcp from homeassistant.components.tesla_wall_connector.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -31,7 +31,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Tesla Wall Connector" assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -52,7 +52,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -73,7 +73,7 @@ async def test_form_other_error( {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -128,7 +128,7 @@ async def test_dhcp_can_finish( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: "1.2.3.4"} diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index 0a10c24a2f6..3d0ff53a4a9 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.threshold.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My threshold sensor" assert result["data"] == {} assert result["options"] == { @@ -68,7 +68,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -80,7 +80,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} @@ -118,7 +118,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "hysteresis") == 0.0 @@ -132,7 +132,7 @@ async def test_options(hass: HomeAssistant) -> None: "upper": 20.0, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_id": input_sensor, "hysteresis": 0.0, diff --git a/tests/components/tod/test_config_flow.py b/tests/components/tod/test_config_flow.py index 35f9ef0d5bd..e215a4f1ae9 100644 --- a/tests/components/tod/test_config_flow.py +++ b/tests/components/tod/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.tod.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -35,7 +35,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My tod" assert result["data"] == {} assert result["options"] == { @@ -85,7 +85,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "after_time") == "10:00" @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: "before_time": "17:05", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "after_time": "10:00", "before_time": "17:05", diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 9991decc511..38542ad7db5 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.tolo.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( ip="127.0.0.2", macaddress="00:11:22:33:44:55", hostname="mock_hostname" @@ -37,7 +33,7 @@ async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock): data={CONF_HOST: "127.0.0.1"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {"base": "cannot_connect"} @@ -48,7 +44,7 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert "flow_id" in result @@ -59,7 +55,7 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): user_input={CONF_HOST: "127.0.0.2"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == SOURCE_USER assert result2["errors"] == {"base": "cannot_connect"} assert "flow_id" in result2 @@ -71,7 +67,7 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): user_input={CONF_HOST: "127.0.0.1"}, ) - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "TOLO Sauna" assert result3["data"][CONF_HOST] == "127.0.0.1" @@ -83,7 +79,7 @@ async def test_dhcp(hass: HomeAssistant, toloclient: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -91,7 +87,7 @@ async def test_dhcp(hass: HomeAssistant, toloclient: Mock): user_input={}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "TOLO Sauna" assert result["data"][CONF_HOST] == "127.0.0.2" assert result["result"].unique_id == "00:11:22:33:44:55" @@ -104,4 +100,4 @@ async def test_dhcp_invalid_device(hass: HomeAssistant, toloclient: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index a3792238fb2..77ab43a05b5 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components import dhcp from homeassistant.components.tplink import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( ALIAS, @@ -256,7 +256,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data={CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_NAME: ALIAS}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_single_discovery(): @@ -268,7 +268,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_single_discovery(): @@ -280,7 +280,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with _patch_discovery(no_device=True), _patch_single_discovery(no_device=True): @@ -292,7 +292,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -318,7 +318,7 @@ async def test_discovered_by_dhcp_or_discovery(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_single_discovery(), patch( @@ -358,5 +358,5 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/trafikverket_ferry/test_config_flow.py b/tests/components/trafikverket_ferry/test_config_flow.py index c75937999d0..47befa55fed 100644 --- a/tests/components/trafikverket_ferry/test_config_flow.py +++ b/tests/components/trafikverket_ferry/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.trafikverket_ferry.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -50,7 +46,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Ekerö to Slagsta at 10:00" assert result2["data"] == { "api_key": "1234567890", @@ -91,7 +87,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -137,7 +133,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -152,7 +148,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -220,7 +216,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -235,7 +231,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index 37788fc285b..8a4baa18ec2 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.trafikverket_train.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -50,7 +46,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Stockholm C to Uppsala C at 10:00" assert result2["data"] == { "api_key": "1234567890", @@ -86,7 +82,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -107,7 +103,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -140,7 +136,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -165,7 +161,7 @@ async def test_flow_fails_incorrect_time(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result5["type"] == RESULT_TYPE_FORM + assert result5["type"] == FlowResultType.FORM assert result5["step_id"] == config_entries.SOURCE_USER with patch( @@ -210,7 +206,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -225,7 +221,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -297,7 +293,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -312,7 +308,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index 458652f8175..89fac5fd5df 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -8,7 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType DOMAIN = "trafikverket_weatherstation" CONF_STATION = "station" @@ -76,7 +76,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index aec0f29e590..05e20dd06bd 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.twentemilieu.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -33,7 +29,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -46,7 +42,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_ID: 12345, @@ -70,7 +66,7 @@ async def test_invalid_address( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -83,7 +79,7 @@ async def test_invalid_address( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_address"} assert "flow_id" in result2 @@ -97,7 +93,7 @@ async def test_invalid_address( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_ID: 12345, @@ -124,7 +120,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert result.get("errors") == {"base": "cannot_connect"} @@ -146,5 +142,5 @@ async def test_address_already_set_up( }, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 7369816fdc7..66945e972de 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -10,11 +10,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components.ukraine_alarm.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -60,10 +56,10 @@ async def test_state(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -77,7 +73,7 @@ async def test_state(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "State 1" assert result3["data"] == { "region": "1", @@ -91,10 +87,10 @@ async def test_state_district(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -102,7 +98,7 @@ async def test_state_district(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -116,7 +112,7 @@ async def test_state_district(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["title"] == "District 2.2" assert result4["data"] == { "region": "2.2", @@ -130,10 +126,10 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -141,7 +137,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -155,7 +151,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["title"] == "State 2" assert result4["data"] == { "region": "2", @@ -169,12 +165,12 @@ async def test_state_district_community(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -182,7 +178,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3", }, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM result4 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -190,7 +186,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3.2", }, ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -204,7 +200,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result5["type"] == RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == FlowResultType.CREATE_ENTRY assert result5["title"] == "Community 3.2.1" assert result5["data"] == { "region": "3.2.1", @@ -235,7 +231,7 @@ async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "rate_limit" @@ -247,7 +243,7 @@ async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -257,7 +253,7 @@ async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -269,7 +265,7 @@ async def test_unknown_client_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -279,7 +275,7 @@ async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "timeout" @@ -291,5 +287,5 @@ async def test_no_regions_returned( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 3d561f2d781..5304e05fe13 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -19,11 +19,7 @@ from homeassistant.components.unifiprotect.const import ( ) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr from . import ( @@ -66,7 +62,7 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch( @@ -86,7 +82,7 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "1.1.1.1", @@ -118,7 +114,7 @@ async def test_form_version_too_old(hass: HomeAssistant, old_nvr: NVR) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "protect_version"} @@ -141,7 +137,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -164,7 +160,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -191,7 +187,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: "entry_id": mock_config.entry_id, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -211,7 +207,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} assert result2["step_id"] == "reauth_confirm" @@ -227,7 +223,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: }, ) - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -258,7 +254,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - assert mock_config.state == config_entries.ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "init" @@ -267,7 +263,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - {CONF_DISABLE_RTSP: True, CONF_ALL_UPDATES: True, CONF_OVERRIDE_CHOST: True}, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == { "all_updates": True, "disable_rtsp": True, @@ -295,7 +291,7 @@ async def test_discovered_by_ssdp_or_dhcp( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "discovery_started" @@ -312,7 +308,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -338,7 +334,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DIRECT_CONNECT_DOMAIN, @@ -378,7 +374,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == DIRECT_CONNECT_DOMAIN @@ -413,7 +409,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "127.0.0.1" @@ -448,7 +444,7 @@ async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_ ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "1.2.2.2" @@ -479,7 +475,7 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "a.hostname" @@ -495,7 +491,7 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -521,7 +517,7 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -547,7 +543,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -573,7 +569,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -612,7 +608,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -642,7 +638,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -680,7 +676,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -716,7 +712,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -742,7 +738,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "nomatchsameip.ui.direct", @@ -785,7 +781,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -806,5 +802,5 @@ async def test_discovery_can_be_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/uptime/test_config_flow.py b/tests/components/uptime/test_config_flow.py index 69ba00f6ac8..9db909003e9 100644 --- a/tests/components/uptime/test_config_flow.py +++ b/tests/components/uptime/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.uptime.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +21,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -34,7 +30,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Uptime" assert result2.get("data") == {} @@ -52,7 +48,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -67,6 +63,6 @@ async def test_import_flow( data={CONF_NAME: "My Uptime"}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "My Uptime" assert result.get("data") == {} diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index c477e1bbc65..b058ceecbdc 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant import config_entries from homeassistant.components.uptimerobot.const import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .common import ( MOCK_UPTIMEROBOT_ACCOUNT, @@ -34,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -51,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["result"].unique_id == MOCK_UPTIMEROBOT_UNIQUE_ID - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_UPTIMEROBOT_ACCOUNT["email"] assert result2["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +59,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -76,7 +72,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == "not_main_key" @@ -103,7 +99,7 @@ async def test_form_exception_thrown(hass: HomeAssistant, exception, error_key) {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == error_key @@ -136,7 +132,7 @@ async def test_user_unique_id_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -153,7 +149,7 @@ async def test_user_unique_id_already_exists( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -174,7 +170,7 @@ async def test_reauthentication( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -192,7 +188,7 @@ async def test_reauthentication( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -213,7 +209,7 @@ async def test_reauthentication_failure( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -232,7 +228,7 @@ async def test_reauthentication_failure( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == "unknown" @@ -255,7 +251,7 @@ async def test_reauthentication_failure_no_existing_entry( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -273,7 +269,7 @@ async def test_reauthentication_failure_no_existing_entry( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_failed_existing" @@ -294,7 +290,7 @@ async def test_reauthentication_failure_account_not_matching( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -316,5 +312,5 @@ async def test_reauthentication_failure_account_not_matching( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == "reauth_failed_matching_account" diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 53f9d814f2f..29a8cab9677 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.utility_meter.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -38,7 +38,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -73,7 +73,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -88,7 +88,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -117,7 +117,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -132,7 +132,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"]["base"] == "tariffs_not_unique" @@ -172,7 +172,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "source") == input_sensor1_entity_id @@ -181,7 +181,7 @@ async def test_options(hass: HomeAssistant) -> None: result["flow_id"], user_input={"source": input_sensor2_entity_id}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "cycle": "monthly", "delta_values": False, diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py index ee6b05f1f8b..b0c951383b8 100644 --- a/tests/components/vallox/test_config_flow.py +++ b/tests/components/vallox/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.vallox.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +19,7 @@ async def test_form_no_input(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None @@ -33,7 +29,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert init["type"] == RESULT_TYPE_FORM + assert init["type"] == FlowResultType.FORM assert init["errors"] is None with patch( @@ -49,7 +45,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Vallox" assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} assert len(mock_setup_entry.mock_calls) == 1 @@ -67,7 +63,7 @@ async def test_form_invalid_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "invalid_host"} @@ -87,7 +83,7 @@ async def test_form_vallox_api_exception_cannot_connect(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -107,7 +103,7 @@ async def test_form_os_error_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -127,7 +123,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "unknown"} @@ -152,7 +148,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -174,7 +170,7 @@ async def test_import_with_custom_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == name assert result["data"] == {"host": "1.2.3.4", "name": "Vallox 90 MV"} assert len(mock_setup_entry.mock_calls) == 1 @@ -196,7 +192,7 @@ async def test_import_without_custom_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Vallox" assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} assert len(mock_setup_entry.mock_calls) == 1 @@ -213,7 +209,7 @@ async def test_import_invalid_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_host" @@ -237,7 +233,7 @@ async def test_import_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -256,7 +252,7 @@ async def test_import_cannot_connect_os_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -275,7 +271,7 @@ async def test_import_cannot_connect_vallox_api_exception(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -294,5 +290,5 @@ async def test_import_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/venstar/test_config_flow.py b/tests/components/venstar/test_config_flow.py index f568655ec8d..f8f66f1b388 100644 --- a/tests/components/venstar/test_config_flow.py +++ b/tests/components/venstar/test_config_flow.py @@ -13,11 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import VenstarColorTouchMock @@ -41,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -57,7 +53,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -77,7 +73,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -96,7 +92,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -108,7 +104,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -124,5 +120,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 8448bddb288..759a9049969 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.vera import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, mock_registry @@ -23,7 +23,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result = await hass.config_entries.flow.async_configure( @@ -34,7 +34,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: CONF_EXCLUDE: "14 15", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -63,7 +63,7 @@ async def test_async_step_import_success(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -94,7 +94,7 @@ async def test_async_step_import_success_with_legacy_unique_id( data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 356cd8fd63c..03fa0fa82cc 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -17,11 +17,7 @@ from homeassistant.components.verisure.const import ( ) from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -38,7 +34,7 @@ async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -61,7 +57,7 @@ async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "ascending (12345th street)" assert result2["data"] == { CONF_GIID: "12345", @@ -79,7 +75,7 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -100,7 +96,7 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non await hass.async_block_till_done() assert result2["step_id"] == "installation" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] is None with patch( @@ -112,7 +108,7 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "descending (54321th street)" assert result3["data"] == { CONF_GIID: "54321", @@ -143,7 +139,7 @@ async def test_invalid_login(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -167,7 +163,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -182,7 +178,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -209,7 +205,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -228,7 +224,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { CONF_GIID: "12345", @@ -277,7 +273,7 @@ async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -318,7 +314,7 @@ async def test_reauth_flow_unknown_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -366,7 +362,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -374,7 +370,7 @@ async def test_options_flow( user_input=input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == output @@ -396,7 +392,7 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -408,6 +404,6 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "code_format_mismatch"} diff --git a/tests/components/version/test_config_flow.py b/tests/components/version/test_config_flow.py index 64296498f35..9745272626c 100644 --- a/tests/components/version/test_config_flow.py +++ b/tests/components/version/test_config_flow.py @@ -19,7 +19,7 @@ from homeassistant.components.version.const import ( ) from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from homeassistant.util import dt from .common import MOCK_VERSION, MOCK_VERSION_DATA, setup_version_integration @@ -50,7 +50,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": False}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.version.async_setup_entry", @@ -62,7 +62,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == VERSION_SOURCE_DOCKER_HUB assert result2["data"] == { **DEFAULT_CONFIGURATION, @@ -78,7 +78,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -86,11 +86,11 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -102,7 +102,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_PYPI assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -119,7 +119,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -127,11 +127,11 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -143,7 +143,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_DOCKER_HUB assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -160,7 +160,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -168,11 +168,11 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -185,7 +185,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"{VERSION_SOURCE_VERSIONS} Dev" assert result["data"] == { **DEFAULT_CONFIGURATION, diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index f86644b3447..66ca5c2cb20 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.vlc_telnet.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -56,7 +52,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( @@ -73,7 +69,7 @@ async def test_user_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == entry_data["host"] assert result["data"] == entry_data assert len(mock_setup_entry.mock_calls) == 1 @@ -98,7 +94,7 @@ async def test_abort_already_configured(hass: HomeAssistant, source: str) -> Non data=entry_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -137,7 +133,7 @@ async def test_errors( {"password": "test-password"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -177,7 +173,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert dict(entry.data) == {**entry_data, "password": "new-password"} @@ -232,7 +228,7 @@ async def test_reauth_errors( {"password": "test-password"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -263,11 +259,11 @@ async def test_hassio_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == test_data.config["name"] assert result2["data"] == test_data.config assert len(mock_setup_entry.mock_calls) == 1 @@ -294,7 +290,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT @pytest.mark.parametrize( @@ -336,9 +332,9 @@ async def test_hassio_errors( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == error diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index 8027759738f..5e97bd2a396 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -22,11 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType @pytest.mark.parametrize( @@ -43,7 +39,7 @@ async def test_auth_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config_auth ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} @@ -78,7 +74,7 @@ async def test_coordinate_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == errors @@ -95,7 +91,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -130,7 +126,7 @@ async def test_show_form_coordinates( result["flow_id"], user_input=config_location_type ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "coordinates" assert result["errors"] is None @@ -140,7 +136,7 @@ async def test_show_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -166,7 +162,7 @@ async def test_step_reauth( user_input={CONF_PASSWORD: "password"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -188,7 +184,7 @@ async def test_step_user_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", @@ -213,7 +209,7 @@ async def test_step_user_home( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index 012682615dc..b5ad3f4cc2b 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -17,11 +17,7 @@ from homeassistant.const import ( CONF_SOURCE, CONF_UNIQUE_ID, ) -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import setup_webostv from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_APPS, MOCK_INPUTS, TV_NAME @@ -55,7 +51,7 @@ async def test_form(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -65,7 +61,7 @@ async def test_form(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_init( @@ -75,7 +71,7 @@ async def test_form(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" with patch("homeassistant.components.webostv.async_setup_entry", return_value=True): @@ -85,7 +81,7 @@ async def test_form(hass, client): await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == TV_NAME @@ -115,7 +111,7 @@ async def test_options_flow_live_tv_in_apps(hass, client, apps, inputs): result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -124,7 +120,7 @@ async def test_options_flow_live_tv_in_apps(hass, client, apps, inputs): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"] @@ -136,7 +132,7 @@ async def test_options_flow_cannot_retrieve(hass, client): result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve"} @@ -154,7 +150,7 @@ async def test_form_cannot_connect(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -172,7 +168,7 @@ async def test_form_pairexception(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "error_pairing" @@ -187,7 +183,7 @@ async def test_entry_already_configured(hass, client): data=MOCK_YAML_CONFIG, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -201,7 +197,7 @@ async def test_form_ssdp(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" @@ -216,7 +212,7 @@ async def test_ssdp_in_progress(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" result2 = await hass.config_entries.flow.async_init( @@ -224,7 +220,7 @@ async def test_ssdp_in_progress(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -239,7 +235,7 @@ async def test_ssdp_update_uuid(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:] @@ -258,7 +254,7 @@ async def test_ssdp_not_update_uuid(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "pairing" assert entry.unique_id is None @@ -276,7 +272,7 @@ async def test_form_abort_uuid_configured(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_config = { @@ -291,7 +287,7 @@ async def test_form_abort_uuid_configured(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -300,6 +296,6 @@ async def test_form_abort_uuid_configured(hass, client): await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "new_host" diff --git a/tests/components/whois/test_config_flow.py b/tests/components/whois/test_config_flow.py index 4bf6d7e8731..7250a9d1567 100644 --- a/tests/components/whois/test_config_flow.py +++ b/tests/components/whois/test_config_flow.py @@ -13,11 +13,7 @@ from homeassistant.components.whois.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -32,7 +28,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -41,7 +37,7 @@ async def test_full_user_flow( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Example.com" assert result2.get("data") == {CONF_DOMAIN: "example.com"} @@ -73,7 +69,7 @@ async def test_full_flow_with_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -83,7 +79,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": reason} assert "flow_id" in result2 @@ -97,7 +93,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Example.com" assert result3.get("data") == {CONF_DOMAIN: "example.com"} @@ -120,7 +116,7 @@ async def test_already_configured( data={CONF_DOMAIN: "HOME-Assistant.io"}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index 655642dffc1..b48641d7ea1 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -7,11 +7,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.wiffi.const import DOMAIN from homeassistant.const import CONF_PORT, CONF_TIMEOUT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -77,7 +73,7 @@ async def test_form(hass, dummy_tcp_server): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER @@ -85,7 +81,7 @@ async def test_form(hass, dummy_tcp_server): result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY async def test_form_addr_in_use(hass, addr_in_use): @@ -98,7 +94,7 @@ async def test_form_addr_in_use(hass, addr_in_use): result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "addr_in_use" @@ -112,7 +108,7 @@ async def test_form_start_server_failed(hass, start_server_failed): result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "start_server_failed" diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index 326224b02c9..a209d55ba99 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -12,11 +12,7 @@ from homeassistant.components.wilight.config_flow import ( from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.wilight import ( @@ -61,7 +57,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -77,7 +73,7 @@ async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -89,7 +85,7 @@ async def test_ssdp_not_wilight_abort_2(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -103,7 +99,7 @@ async def test_ssdp_not_wilight_abort_3( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -117,7 +113,7 @@ async def test_ssdp_not_supported_abort( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_supported_device" @@ -142,7 +138,7 @@ async def test_ssdp_device_exists_abort(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -154,7 +150,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -165,7 +161,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"WL{WILIGHT_ID}" assert result["data"] diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index f37f2ba21a0..75ab1d1b188 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant.components import dhcp from homeassistant.components.wiz.config_flow import CONF_DEVICE from homeassistant.components.wiz.const import DOMAIN from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( FAKE_DIMMABLE_BULB, @@ -85,7 +85,7 @@ async def test_user_flow_enters_dns_name(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "no_ip"} with _patch_wizlight(), patch( @@ -179,7 +179,7 @@ async def test_discovered_by_dhcp_connection_fails(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -256,7 +256,7 @@ async def test_discovered_by_dhcp_or_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with _patch_wizlight( @@ -306,7 +306,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_updates_host( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == FAKE_IP @@ -335,7 +335,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_avoid_waiting_for_ret ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.state is config_entries.ConfigEntryState.LOADED @@ -458,7 +458,7 @@ async def test_setup_via_discovery_exception_finds_nothing(hass): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -476,7 +476,7 @@ async def test_discovery_with_firmware_update(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" # In between discovery and when the user clicks to set it up the firmware diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index 97a4cdfed6b..a53429967e6 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -9,11 +9,7 @@ from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError from homeassistant import config_entries from homeassistant.components.yale_smart_alarm.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -24,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -43,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -85,7 +81,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -104,7 +100,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -138,7 +134,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -156,7 +152,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -218,7 +214,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -237,7 +233,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -265,7 +261,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -273,7 +269,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={"code": "123456", "lock_code_digits": 6}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {"code": "123456", "lock_code_digits": 6} @@ -295,7 +291,7 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -304,7 +300,7 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: user_input={"code": "123", "lock_code_digits": 6}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "code_format_mismatch"} @@ -313,5 +309,5 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: user_input={"code": "123456", "lock_code_digits": 6}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {"code": "123456", "lock_code_digits": 6} diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 7d9acc670b5..391c969d893 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -23,7 +23,7 @@ from homeassistant.components.yeelight.const import ( ) from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( CAPABILITIES, @@ -475,7 +475,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -489,7 +489,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_discovery_interval(), patch( @@ -503,7 +503,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with _patch_discovery( @@ -519,7 +519,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -558,7 +558,7 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -587,7 +587,7 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -626,7 +626,7 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -642,7 +642,7 @@ async def test_discovered_ssdp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -671,7 +671,7 @@ async def test_discovered_ssdp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -689,7 +689,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -720,7 +720,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_bulb = _mocked_bulb() @@ -734,7 +734,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -756,7 +756,7 @@ async def test_discovery_updates_ip(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -784,7 +784,7 @@ async def test_discovery_updates_ip_no_reload_setup_in_progress(hass: HomeAssist ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS assert len(mock_setup_entry.mock_calls) == 0 @@ -806,7 +806,7 @@ async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS diff --git a/tests/components/youless/test_config_flows.py b/tests/components/youless/test_config_flows.py index d7d9a39ec6e..5bf119681c4 100644 --- a/tests/components/youless/test_config_flows.py +++ b/tests/components/youless/test_config_flows.py @@ -5,7 +5,7 @@ from urllib.error import URLError from homeassistant.components.youless import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType def _get_mock_youless_api(initialize=None): @@ -25,7 +25,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -42,7 +42,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "localhost" assert len(mocked_youless.mock_calls) == 1 @@ -53,7 +53,7 @@ async def test_not_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -68,5 +68,5 @@ async def test_not_found(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert len(mocked_youless.mock_calls) == 1 diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 285c08d8a3e..84290595f12 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -25,11 +25,7 @@ from homeassistant.config_entries import ( SOURCE_ZEROCONF, ) from homeassistant.const import CONF_SOURCE -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -72,7 +68,7 @@ async def test_discovery(detect_mock, hass): flow["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "socket://192.168.1.200:6638" assert result["data"] == { CONF_DEVICE: { @@ -104,7 +100,7 @@ async def test_zigate_via_zeroconf(probe_mock, hass): flow["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "socket://192.168.1.200:1234" assert result["data"] == { CONF_DEVICE: { @@ -134,7 +130,7 @@ async def test_efr32_via_zeroconf(probe_mock, hass): flow["flow_id"], user_input={"baudrate": 115200} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "socket://192.168.1.200:6638" assert result["data"] == { CONF_DEVICE: { @@ -176,7 +172,7 @@ async def test_discovery_via_zeroconf_ip_change(detect_mock, hass): "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "socket://192.168.1.22:6638", @@ -209,7 +205,7 @@ async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass): "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "socket://192.168.1.22:6638", @@ -231,7 +227,7 @@ async def test_discovery_via_usb(detect_mock, hass): "zha", context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): @@ -240,7 +236,7 @@ async def test_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert "zigbee radio" in result2["title"] assert result2["data"] == { "device": { @@ -267,7 +263,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): "zha", context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): @@ -276,7 +272,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert ( "zigate radio - /dev/ttyZIGBEE, s/n: 1234 - test - 6015:0403" in result2["title"] @@ -304,7 +300,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): "zha", context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): @@ -313,7 +309,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "usb_probe_failed" @@ -338,7 +334,7 @@ async def test_discovery_via_usb_already_setup(detect_mock, hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -372,7 +368,7 @@ async def test_discovery_via_usb_path_changes(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyZIGBEE", @@ -457,7 +453,7 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" @@ -485,7 +481,7 @@ async def test_discovery_via_usb_zha_ignored_updates(detect_mock, hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyZIGBEE", @@ -535,7 +531,7 @@ async def test_user_flow(detect_mock, hass): context={CONF_SOURCE: SOURCE_USER}, data={zigpy.config.CONF_DEVICE_PATH: port_select}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"].startswith(port.description) assert result["data"] == {CONF_RADIO_TYPE: "test_radio"} assert detect_mock.await_count == 1 @@ -559,7 +555,7 @@ async def test_user_flow_not_detected(detect_mock, hass): data={zigpy.config.CONF_DEVICE_PATH: port_select}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_radio" assert detect_mock.await_count == 1 assert detect_mock.await_args[0][0] == port.device @@ -573,7 +569,7 @@ async def test_user_flow_show_form(hass): context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -585,7 +581,7 @@ async def test_user_flow_show_manual(hass): context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_radio" @@ -597,7 +593,7 @@ async def test_user_flow_manual(hass): context={CONF_SOURCE: SOURCE_USER}, data={zigpy.config.CONF_DEVICE_PATH: config_flow.CONF_MANUAL_PATH}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_radio" @@ -608,7 +604,7 @@ async def test_pick_radio_flow(hass, radio_type): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: "pick_radio"}, data={CONF_RADIO_TYPE: radio_type} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "port_config" @@ -708,7 +704,7 @@ async def test_user_port_config_fail(probe_mock, hass): result["flow_id"], user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "port_config" assert result["errors"]["base"] == "cannot_connect" assert probe_mock.await_count == 1 @@ -793,7 +789,7 @@ async def test_hardware_not_onboarded(hass): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyAMA1" assert result["data"] == { CONF_DEVICE: { @@ -823,14 +819,14 @@ async def test_hardware_onboarded(hass): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm_hardware" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyAMA1" assert result["data"] == { CONF_DEVICE: { @@ -861,7 +857,7 @@ async def test_hardware_already_setup(hass): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -875,5 +871,5 @@ async def test_hardware_invalid_data(hass, data): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_hardware_data" diff --git a/tests/components/zwave_me/test_config_flow.py b/tests/components/zwave_me/test_config_flow.py index 36a73c4fc06..57ead9a9e60 100644 --- a/tests/components/zwave_me/test_config_flow.py +++ b/tests/components/zwave_me/test_config_flow.py @@ -5,12 +5,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.zwave_me.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, - FlowResult, -) +from homeassistant.data_entry_flow import FlowResult, FlowResultType from tests.common import MockConfigEntry @@ -42,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -53,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -76,7 +71,7 @@ async def test_zeroconf(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -87,7 +82,7 @@ async def test_zeroconf(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -104,7 +99,7 @@ async def test_error_handling_zeroconf(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_valid_uuid_set" @@ -114,7 +109,7 @@ async def test_handle_error_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -145,7 +140,7 @@ async def test_duplicate_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -154,7 +149,7 @@ async def test_duplicate_user(hass: HomeAssistant): "token": "test-token", }, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -181,5 +176,5 @@ async def test_duplicate_zeroconf(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 03da4d36853..1970d883efc 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -17,7 +17,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo, FlowResult +from homeassistant.data_entry_flow import BaseServiceInfo, FlowResult, FlowResultType from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, @@ -1659,7 +1659,7 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" @@ -1704,7 +1704,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" @@ -1721,7 +1721,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "2.2.2.2" assert entry.data["additional"] == "data" @@ -1773,7 +1773,7 @@ async def test_unique_id_from_discovery_in_setup_retry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(async_reload.mock_calls) == 0 @@ -1792,7 +1792,7 @@ async def test_unique_id_from_discovery_in_setup_retry(hass, manager): ) await hass.async_block_till_done() - assert discovery_result["type"] == RESULT_TYPE_ABORT + assert discovery_result["type"] == FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" assert len(async_reload.mock_calls) == 1 @@ -1833,7 +1833,7 @@ async def test_unique_id_not_update_existing_entry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "0.0.0.0" assert entry.data["additional"] == "data" @@ -3100,7 +3100,7 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == reason @@ -3250,7 +3250,7 @@ async def test_unique_id_update_while_setup_in_progress( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 301a61700c9..136c97808d3 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -517,7 +517,7 @@ async def test_show_menu(hass, manager, menu_options): return self.async_show_form(step_id="target2") result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result["type"] == data_entry_flow.FlowResultType.MENU assert result["menu_options"] == menu_options assert result["description_placeholders"] == {"name": "Paulus"} assert len(manager.async_progress()) == 1 From c6bff8ae186dfa953832bdc1da5a1b79ff5d58f1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 22:00:19 +0200 Subject: [PATCH 205/820] Remove philips_js from mypy ignore list (#74659) * Remove philips_js from mypy ignore list * Use class attribute --- homeassistant/components/philips_js/__init__.py | 5 +++-- .../components/philips_js/config_flow.py | 6 +++--- .../components/philips_js/device_trigger.py | 4 ++++ homeassistant/components/philips_js/light.py | 2 +- .../components/philips_js/media_player.py | 3 +-- mypy.ini | 15 --------------- script/hassfest/mypy_config.py | 5 ----- 7 files changed, 12 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 29e92a6ffe3..8ff20d8b104 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -108,6 +108,8 @@ class PluggableAction: class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): """Coordinator to update data.""" + config_entry: ConfigEntry + def __init__(self, hass, api: PhilipsTV, options: Mapping) -> None: """Set up the coordinator.""" self.api = api @@ -136,8 +138,7 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): @property def unique_id(self) -> str: """Return the system descriptor.""" - entry: ConfigEntry = self.config_entry - assert entry + entry = self.config_entry if entry.unique_id: return entry.unique_id assert entry.entry_id diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 9785aaf54a3..39f1c78b070 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -23,7 +23,7 @@ from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, async def _validate_input( hass: core.HomeAssistant, host: str, api_version: int -) -> tuple[dict, PhilipsTV]: +) -> PhilipsTV: """Validate the user input allows us to connect.""" hub = PhilipsTV(host, api_version) @@ -44,7 +44,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" super().__init__() - self._current = {} + self._current: dict[str, Any] = {} self._hub: PhilipsTV | None = None self._pair_state: Any = None @@ -62,7 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Attempt to pair with device.""" assert self._hub - errors = {} + errors: dict[str, str] = {} schema = vol.Schema( { vol.Required(CONF_PIN): str, diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index 69a5932576f..eca3158fb15 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -65,6 +65,10 @@ async def async_attach_trigger( } device = registry.async_get(config[CONF_DEVICE_ID]) + if device is None: + raise HomeAssistantError( + f"Device id {config[CONF_DEVICE_ID]} not found in registry" + ) for config_entry_id in device.config_entries: coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN].get( config_entry_id diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 5a32d2954a4..ab4f04a8ddc 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -146,7 +146,7 @@ class PhilipsTVLightEntity( self._hs = None self._brightness = None self._cache_keys = None - self._last_selected_effect: AmbilightEffect = None + self._last_selected_effect: AmbilightEffect | None = None super().__init__(coordinator) self._attr_supported_color_modes = {ColorMode.HS, ColorMode.ONOFF} diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 27cf41abd4f..3d63865fe31 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -80,8 +80,7 @@ class PhilipsTVMediaPlayer( ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api - self._sources = {} - self._channels = {} + self._sources: dict[str, str] = {} self._supports = SUPPORT_PHILIPS_JS self._system = coordinator.system self._attr_name = coordinator.system["name"] diff --git a/mypy.ini b/mypy.ini index b139a40976b..87dd265eeef 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2746,21 +2746,6 @@ ignore_errors = true [mypy-homeassistant.components.onvif.sensor] ignore_errors = true -[mypy-homeassistant.components.philips_js] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.device_trigger] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.light] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.media_player] -ignore_errors = true - [mypy-homeassistant.components.plex.media_player] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 55ed01aaa63..4e11028d574 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -57,11 +57,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.camera", "homeassistant.components.onvif.device", "homeassistant.components.onvif.sensor", - "homeassistant.components.philips_js", - "homeassistant.components.philips_js.config_flow", - "homeassistant.components.philips_js.device_trigger", - "homeassistant.components.philips_js.light", - "homeassistant.components.philips_js.media_player", "homeassistant.components.plex.media_player", "homeassistant.components.profiler", "homeassistant.components.solaredge.config_flow", From 6ebdf0580bfa28ff29d886833a3dd6b26bd84071 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 22:35:06 +0200 Subject: [PATCH 206/820] Remove last occurrences of RESULT_TYPE_* from codebase (#74670) Remove last ocurrances of RESULT_TYPE_* from codebase --- .../templates/config_flow/tests/test_config_flow.py | 10 +++++----- .../config_flow_helper/tests/test_config_flow.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index d1ddc177690..f4413ea4e84 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -5,7 +5,7 @@ from homeassistant import config_entries from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from homeassistant.components.NEW_DOMAIN.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -13,7 +13,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -33,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "1.1.1.1", @@ -62,7 +62,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -85,5 +85,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py index b7bef2c63f4..7d2be8a6d82 100644 --- a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.NEW_DOMAIN.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -32,7 +32,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My NEW_DOMAIN" assert result["data"] == {} assert result["options"] == { @@ -82,7 +82,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "entity_id") == input_sensor_1_entity_id @@ -93,7 +93,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "entity_id": input_sensor_2_entity_id, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_id": input_sensor_2_entity_id, "name": "My NEW_DOMAIN", From 3a61a0de2e12f4d104284fc858273c71b4d5d3b4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 22:38:10 +0200 Subject: [PATCH 207/820] Standardize EntityDescription in DSMR (#74671) --- homeassistant/components/dsmr/const.py | 273 ---------------------- homeassistant/components/dsmr/models.py | 14 -- homeassistant/components/dsmr/sensor.py | 292 +++++++++++++++++++++++- 3 files changed, 286 insertions(+), 293 deletions(-) delete mode 100644 homeassistant/components/dsmr/models.py diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 9f08e812e04..5e1a54aedc4 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -3,13 +3,7 @@ from __future__ import annotations import logging -from dsmr_parser import obis_references - -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import Platform -from homeassistant.helpers.entity import EntityCategory - -from .models import DSMRSensorEntityDescription DOMAIN = "dsmr" @@ -40,270 +34,3 @@ DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} DSMR_PROTOCOL = "dsmr_protocol" RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol" - -SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( - DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_USAGE, - name="Power Consumption", - device_class=SensorDeviceClass.POWER, - force_update=True, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_DELIVERY, - name="Power Production", - device_class=SensorDeviceClass.POWER, - force_update=True, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_ACTIVE_TARIFF, - name="Power Tariff", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - icon="mdi:flash", - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_1, - name="Energy Consumption (tarif 1)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - device_class=SensorDeviceClass.ENERGY, - force_update=True, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_2, - name="Energy Consumption (tarif 2)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, - name="Energy Production (tarif 1)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, - name="Energy Production (tarif 2)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, - name="Power Consumption Phase L1", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, - name="Power Consumption Phase L2", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, - name="Power Consumption Phase L3", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, - name="Power Production Phase L1", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, - name="Power Production Phase L2", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, - name="Power Production Phase L3", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.SHORT_POWER_FAILURE_COUNT, - name="Short Power Failure Count", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:flash-off", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.LONG_POWER_FAILURE_COUNT, - name="Long Power Failure Count", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:flash-off", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L1_COUNT, - name="Voltage Sags Phase L1", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L2_COUNT, - name="Voltage Sags Phase L2", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L3_COUNT, - name="Voltage Sags Phase L3", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L1_COUNT, - name="Voltage Swells Phase L1", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L2_COUNT, - name="Voltage Swells Phase L2", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L3_COUNT, - name="Voltage Swells Phase L3", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L1, - name="Voltage Phase L1", - device_class=SensorDeviceClass.VOLTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L2, - name="Voltage Phase L2", - device_class=SensorDeviceClass.VOLTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L3, - name="Voltage Phase L3", - device_class=SensorDeviceClass.VOLTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L1, - name="Current Phase L1", - device_class=SensorDeviceClass.CURRENT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L2, - name="Current Phase L2", - device_class=SensorDeviceClass.CURRENT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L3, - name="Current Phase L3", - device_class=SensorDeviceClass.CURRENT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_POWER_PER_PHASE, - name="Max power per phase", - dsmr_versions={"5B"}, - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, - name="Max current per phase", - dsmr_versions={"5B"}, - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_IMPORTED_TOTAL, - name="Energy Consumption (total)", - dsmr_versions={"5L", "5S", "Q3D"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_EXPORTED_TOTAL, - name="Energy Production (total)", - dsmr_versions={"5L", "5S", "Q3D"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.HOURLY_GAS_METER_READING, - name="Gas Consumption", - dsmr_versions={"4", "5", "5L"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.BELGIUM_5MIN_GAS_METER_READING, - name="Gas Consumption", - dsmr_versions={"5B"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.GAS_METER_READING, - name="Gas Consumption", - dsmr_versions={"2.2"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), -) diff --git a/homeassistant/components/dsmr/models.py b/homeassistant/components/dsmr/models.py deleted file mode 100644 index e7b47d8b74d..00000000000 --- a/homeassistant/components/dsmr/models.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Models for the DSMR integration.""" -from __future__ import annotations - -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class DSMRSensorEntityDescription(SensorEntityDescription): - """Represents an DSMR Sensor.""" - - dsmr_versions: set[str] | None = None - is_gas: bool = False diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 9c684493a3f..1337f209d5d 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -4,10 +4,11 @@ from __future__ import annotations import asyncio from asyncio import CancelledError from contextlib import suppress +from dataclasses import dataclass from datetime import timedelta from functools import partial -from dsmr_parser import obis_references as obis_ref +from dsmr_parser import obis_references from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader from dsmr_parser.clients.rfxtrx_protocol import ( create_rfxtrx_dsmr_reader, @@ -16,7 +17,12 @@ from dsmr_parser.clients.rfxtrx_protocol import ( from dsmr_parser.objects import DSMRObject import serial -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -25,7 +31,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import EventType, StateType from homeassistant.util import Throttle @@ -47,13 +53,287 @@ from .const import ( DOMAIN, DSMR_PROTOCOL, LOGGER, - SENSORS, ) -from .models import DSMRSensorEntityDescription UNIT_CONVERSION = {"m3": VOLUME_CUBIC_METERS} +@dataclass +class DSMRSensorEntityDescription(SensorEntityDescription): + """Represents an DSMR Sensor.""" + + dsmr_versions: set[str] | None = None + is_gas: bool = False + + +SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( + DSMRSensorEntityDescription( + key=obis_references.CURRENT_ELECTRICITY_USAGE, + name="Power Consumption", + device_class=SensorDeviceClass.POWER, + force_update=True, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.CURRENT_ELECTRICITY_DELIVERY, + name="Power Production", + device_class=SensorDeviceClass.POWER, + force_update=True, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_ACTIVE_TARIFF, + name="Power Tariff", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + icon="mdi:flash", + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_USED_TARIFF_1, + name="Energy Consumption (tarif 1)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + device_class=SensorDeviceClass.ENERGY, + force_update=True, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_USED_TARIFF_2, + name="Energy Consumption (tarif 2)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, + name="Energy Production (tarif 1)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, + name="Energy Production (tarif 2)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + name="Power Consumption Phase L1", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + name="Power Consumption Phase L2", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + name="Power Consumption Phase L3", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + name="Power Production Phase L1", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + name="Power Production Phase L2", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + name="Power Production Phase L3", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.SHORT_POWER_FAILURE_COUNT, + name="Short Power Failure Count", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:flash-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.LONG_POWER_FAILURE_COUNT, + name="Long Power Failure Count", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:flash-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SAG_L1_COUNT, + name="Voltage Sags Phase L1", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SAG_L2_COUNT, + name="Voltage Sags Phase L2", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SAG_L3_COUNT, + name="Voltage Sags Phase L3", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SWELL_L1_COUNT, + name="Voltage Swells Phase L1", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SWELL_L2_COUNT, + name="Voltage Swells Phase L2", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SWELL_L3_COUNT, + name="Voltage Swells Phase L3", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_VOLTAGE_L1, + name="Voltage Phase L1", + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_VOLTAGE_L2, + name="Voltage Phase L2", + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_VOLTAGE_L3, + name="Voltage Phase L3", + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_CURRENT_L1, + name="Current Phase L1", + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_CURRENT_L2, + name="Current Phase L2", + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_CURRENT_L3, + name="Current Phase L3", + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.BELGIUM_MAX_POWER_PER_PHASE, + name="Max power per phase", + dsmr_versions={"5B"}, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, + name="Max current per phase", + dsmr_versions={"5B"}, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_IMPORTED_TOTAL, + name="Energy Consumption (total)", + dsmr_versions={"5L", "5S", "Q3D"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_EXPORTED_TOTAL, + name="Energy Production (total)", + dsmr_versions={"5L", "5S", "Q3D"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.HOURLY_GAS_METER_READING, + name="Gas Consumption", + dsmr_versions={"4", "5", "5L"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.BELGIUM_5MIN_GAS_METER_READING, + name="Gas Consumption", + dsmr_versions={"5B"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.GAS_METER_READING, + name="Gas Consumption", + dsmr_versions={"2.2"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -240,7 +520,7 @@ class DSMREntity(SensorEntity): if (value := self.get_dsmr_object_attr("value")) is None: return None - if self.entity_description.key == obis_ref.ELECTRICITY_ACTIVE_TARIFF: + if self.entity_description.key == obis_references.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value, self._entry.data[CONF_DSMR_VERSION]) with suppress(TypeError): From a84d92be4d87a52f2fc6952b19a9884e728bec9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jul 2022 15:45:40 -0500 Subject: [PATCH 208/820] Fix climacell/tomorrowio config flow test failure (#74660) --- homeassistant/components/climacell/weather.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 6aee9b54f6c..73ff6361041 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -148,17 +148,16 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional state attributes.""" - wind_gust = self.wind_gust - wind_gust = round( - speed_convert(self.wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), - 4, - ) cloud_cover = self.cloud_cover - return { + attrs = { ATTR_CLOUD_COVER: cloud_cover, - ATTR_WIND_GUST: wind_gust, ATTR_PRECIPITATION_TYPE: self.precipitation_type, } + if (wind_gust := self.wind_gust) is not None: + attrs[ATTR_WIND_GUST] = round( + speed_convert(wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), 4 + ) + return attrs @property @abstractmethod From 5dd8dfb4dc768782f3077795bdf28ceefa7154c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Fri, 8 Jul 2022 00:46:23 +0200 Subject: [PATCH 209/820] Update kaiterra-async-client to 1.0.0 (#74677) --- homeassistant/components/kaiterra/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json index 9f2a4c0013f..94b861524eb 100644 --- a/homeassistant/components/kaiterra/manifest.json +++ b/homeassistant/components/kaiterra/manifest.json @@ -2,7 +2,7 @@ "domain": "kaiterra", "name": "Kaiterra", "documentation": "https://www.home-assistant.io/integrations/kaiterra", - "requirements": ["kaiterra-async-client==0.0.2"], + "requirements": ["kaiterra-async-client==1.0.0"], "codeowners": ["@Michsior14"], "iot_class": "cloud_polling", "loggers": ["kaiterra_async_client"] diff --git a/requirements_all.txt b/requirements_all.txt index 7cc20779883..9f9423496fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -912,7 +912,7 @@ jellyfin-apiclient-python==1.8.1 jsonpath==0.82 # homeassistant.components.kaiterra -kaiterra-async-client==0.0.2 +kaiterra-async-client==1.0.0 # homeassistant.components.keba keba-kecontact==1.1.0 From 405d323709edc13501b1710cdc72959883fffa8c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 8 Jul 2022 00:27:47 +0000 Subject: [PATCH 210/820] [ci skip] Translation update --- .../accuweather/translations/ja.json | 2 +- .../components/aemet/translations/ja.json | 4 ++-- .../components/airly/translations/ja.json | 2 +- .../components/airnow/translations/ja.json | 2 +- .../components/airzone/translations/ja.json | 2 +- .../aladdin_connect/translations/ja.json | 4 ++-- .../components/anthemav/translations/nl.json | 18 ++++++++++++++++ .../components/apple_tv/translations/ja.json | 2 +- .../aussie_broadband/translations/ja.json | 2 +- .../azure_event_hub/translations/ja.json | 2 +- .../components/blink/translations/ja.json | 2 +- .../components/bosch_shc/translations/ja.json | 4 ++-- .../components/braviatv/translations/ja.json | 2 +- .../components/brunt/translations/ja.json | 4 ++-- .../cloudflare/translations/ja.json | 2 +- .../components/cpuspeed/translations/ja.json | 2 +- .../components/deluge/translations/ja.json | 2 +- .../devolo_home_network/translations/ja.json | 2 +- .../components/dexcom/translations/ja.json | 2 +- .../dialogflow/translations/ja.json | 2 +- .../components/dlna_dmr/translations/ja.json | 2 +- .../components/dunehd/translations/ja.json | 2 +- .../components/elmax/translations/ja.json | 2 +- .../forked_daapd/translations/ja.json | 4 ++-- .../geocaching/translations/ja.json | 4 ++-- .../components/github/translations/ja.json | 4 ++-- .../components/google/translations/ja.json | 4 ++-- .../homeassistant/translations/nl.json | 1 + .../homekit_controller/translations/ja.json | 2 +- .../components/icloud/translations/ja.json | 4 ++-- .../integration/translations/ja.json | 4 ++-- .../components/isy994/translations/ja.json | 2 +- .../components/kodi/translations/ja.json | 2 +- .../components/laundrify/translations/ja.json | 4 ++-- .../lg_soundbar/translations/nl.json | 18 ++++++++++++++++ .../components/life360/translations/ja.json | 2 +- .../components/life360/translations/nl.json | 7 +++++++ .../components/lyric/translations/ja.json | 4 ++-- .../components/melcloud/translations/ja.json | 2 +- .../mobile_app/translations/ja.json | 2 +- .../modem_callerid/translations/ja.json | 4 ++-- .../components/mullvad/translations/ja.json | 2 +- .../components/mysensors/translations/ja.json | 2 +- .../components/nam/translations/ja.json | 4 ++-- .../components/nest/translations/ja.json | 4 ++-- .../components/netatmo/translations/ja.json | 4 ++-- .../components/nextdns/translations/de.json | 5 +++++ .../components/nextdns/translations/et.json | 5 +++++ .../components/nextdns/translations/ja.json | 5 +++++ .../components/nextdns/translations/nl.json | 21 +++++++++++++++++++ .../nfandroidtv/translations/ja.json | 2 +- .../components/notion/translations/ja.json | 2 +- .../components/nuki/translations/ja.json | 4 ++-- .../openweathermap/translations/ja.json | 4 ++-- .../components/picnic/translations/ja.json | 2 +- .../components/plugwise/translations/ja.json | 2 +- .../components/qnap_qsw/translations/nl.json | 6 ++++++ .../components/renault/translations/ja.json | 2 +- .../components/ridwell/translations/ja.json | 2 +- .../rtsp_to_webrtc/translations/ja.json | 2 +- .../components/sense/translations/ja.json | 4 ++-- .../simplepush/translations/nl.json | 3 +++ .../simplisafe/translations/ja.json | 2 +- .../components/sleepiq/translations/ja.json | 4 ++-- .../components/smappee/translations/ja.json | 4 ++-- .../smartthings/translations/ja.json | 4 ++-- .../components/smarttub/translations/ja.json | 4 ++-- .../somfy_mylink/translations/ja.json | 2 +- .../components/sonarr/translations/ja.json | 4 ++-- .../soundtouch/translations/nl.json | 17 +++++++++++++++ .../components/spotify/translations/ja.json | 6 +++--- .../steam_online/translations/ja.json | 4 ++-- .../components/syncthing/translations/ja.json | 2 +- .../synology_dsm/translations/ja.json | 2 +- .../totalconnect/translations/ja.json | 2 +- .../components/tractive/translations/ja.json | 2 +- .../transmission/translations/ja.json | 2 +- .../ukraine_alarm/translations/ja.json | 2 +- .../components/unifi/translations/ja.json | 4 ++-- .../uptimerobot/translations/ja.json | 4 ++-- .../components/vicare/translations/ja.json | 2 +- .../components/vulcan/translations/ja.json | 4 ++-- .../components/watttime/translations/ja.json | 2 +- .../waze_travel_time/translations/ja.json | 2 +- .../components/whois/translations/ja.json | 2 +- .../components/withings/translations/ja.json | 2 +- .../components/wiz/translations/ja.json | 2 +- .../components/wled/translations/ja.json | 2 +- .../xiaomi_miio/translations/ja.json | 6 +++--- .../components/yolink/translations/ja.json | 4 ++-- .../components/zwave_js/translations/ja.json | 2 +- 91 files changed, 219 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/nl.json create mode 100644 homeassistant/components/lg_soundbar/translations/nl.json create mode 100644 homeassistant/components/nextdns/translations/nl.json create mode 100644 homeassistant/components/soundtouch/translations/nl.json diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index 21ec4e6068d..c7ea9f1d264 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "create_entry": { - "default": "\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u69cb\u6210\u5f8c\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u305d\u308c\u3089\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002" + "default": "\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u69cb\u6210\u5f8c\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u305d\u308c\u3089\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json index fd79c699cee..1279f90beaa 100644 --- a/homeassistant/components/aemet/translations/ja.json +++ b/homeassistant/components/aemet/translations/ja.json @@ -12,9 +12,9 @@ "api_key": "API\u30ad\u30fc", "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", - "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" + "name": "\u7d71\u5408\u306e\u540d\u524d" }, - "description": "AEMET OpenData\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "AEMET OpenData\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 6ff5e2216a6..5116cac487a 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -15,7 +15,7 @@ "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, - "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/airnow/translations/ja.json b/homeassistant/components/airnow/translations/ja.json index 17a1b14e164..6cf1e763eda 100644 --- a/homeassistant/components/airnow/translations/ja.json +++ b/homeassistant/components/airnow/translations/ja.json @@ -17,7 +17,7 @@ "longitude": "\u7d4c\u5ea6", "radius": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u534a\u5f84(\u30de\u30a4\u30eb; \u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/airzone/translations/ja.json b/homeassistant/components/airzone/translations/ja.json index 28ebf809dbf..68bee99ba29 100644 --- a/homeassistant/components/airzone/translations/ja.json +++ b/homeassistant/components/airzone/translations/ja.json @@ -13,7 +13,7 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" }, - "description": "Airzone\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "description": "Airzone\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/aladdin_connect/translations/ja.json b/homeassistant/components/aladdin_connect/translations/ja.json index 196b7f1d066..8859f24cd54 100644 --- a/homeassistant/components/aladdin_connect/translations/ja.json +++ b/homeassistant/components/aladdin_connect/translations/ja.json @@ -13,8 +13,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "Aladdin Connect\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Aladdin Connect\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/anthemav/translations/nl.json b/homeassistant/components/anthemav/translations/nl.json new file mode 100644 index 00000000000..c09dde1bfc3 --- /dev/null +++ b/homeassistant/components/anthemav/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index 3661cbeedb3..860bf4c961b 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -22,7 +22,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eApple TV\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002 \n\n **\u51e6\u7406\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u8907\u6570\u306ePIN\u30b3\u30fc\u30c9\u306e\u5165\u529b\u304c\u5fc5\u8981\u306b\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002** \n\n\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Apple TV\u306e\u96fb\u6e90\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u306f *\u3067\u304d\u306a\u3044* \u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002 Home Assistant\u306e\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u307f\u304c\u30aa\u30d5\u306b\u306a\u308a\u307e\u3059\uff01", + "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eApple TV\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002 \n\n **\u51e6\u7406\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u8907\u6570\u306ePIN\u30b3\u30fc\u30c9\u306e\u5165\u529b\u304c\u5fc5\u8981\u306b\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002** \n\n\u3053\u306e\u7d71\u5408\u3067\u306f\u3001Apple TV\u306e\u96fb\u6e90\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u306f *\u3067\u304d\u306a\u3044* \u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002 Home Assistant\u306e\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u307f\u304c\u30aa\u30d5\u306b\u306a\u308a\u307e\u3059\uff01", "title": "Apple TV\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" }, "pair_no_pin": { diff --git a/homeassistant/components/aussie_broadband/translations/ja.json b/homeassistant/components/aussie_broadband/translations/ja.json index d8351bbc98b..fb2f908851a 100644 --- a/homeassistant/components/aussie_broadband/translations/ja.json +++ b/homeassistant/components/aussie_broadband/translations/ja.json @@ -16,7 +16,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "service": { "data": { diff --git a/homeassistant/components/azure_event_hub/translations/ja.json b/homeassistant/components/azure_event_hub/translations/ja.json index d8c0407fbc5..b504f86a3bd 100644 --- a/homeassistant/components/azure_event_hub/translations/ja.json +++ b/homeassistant/components/azure_event_hub/translations/ja.json @@ -32,7 +32,7 @@ "event_hub_instance_name": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u540d", "use_connection_string": "\u63a5\u7d9a\u6587\u5b57\u5217\u3092\u4f7f\u7528\u3059\u308b" }, - "title": "Azure Event Hub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Azure Event Hub\u7d71\u5408\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json index 40724c01d42..0ff7c2dc1b4 100644 --- a/homeassistant/components/blink/translations/ja.json +++ b/homeassistant/components/blink/translations/ja.json @@ -32,7 +32,7 @@ "data": { "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" }, - "description": "Blink\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a", + "description": "Blink\u7d71\u5408\u306e\u8a2d\u5b9a", "title": "Blink \u30aa\u30d7\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index da4b471c621..6d7883de038 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -22,8 +22,8 @@ } }, "reauth_confirm": { - "description": "Bosch_shc\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Bosch_shc\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 860fd5c89ea..263de9e35b0 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -21,7 +21,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30bd\u30cb\u30fc Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u30bd\u30cb\u30fc Bravia TV\u306e\u7d71\u5408\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } }, diff --git a/homeassistant/components/brunt/translations/ja.json b/homeassistant/components/brunt/translations/ja.json index a0c477443b8..ac50c52ee09 100644 --- a/homeassistant/components/brunt/translations/ja.json +++ b/homeassistant/components/brunt/translations/ja.json @@ -15,14 +15,14 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {username}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "title": "Brunt\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Brunt\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 81fdf638148..1057d4e7bc5 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -28,7 +28,7 @@ "data": { "api_token": "API\u30c8\u30fc\u30af\u30f3" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u5185\u306e\u3059\u3079\u3066\u306e\u30be\u30fc\u30f3\u306b\u5bfe\u3059\u308b\u3001 Zone:Zone:Read \u304a\u3088\u3073\u3001Zone:DNS:Edit\u306e\u6a29\u9650\u3067\u4f5c\u6210\u3055\u308c\u305fAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002", + "description": "\u3053\u306e\u7d71\u5408\u306b\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u5185\u306e\u3059\u3079\u3066\u306e\u30be\u30fc\u30f3\u306b\u5bfe\u3059\u308b\u3001 Zone:Zone:Read \u304a\u3088\u3073\u3001Zone:DNS:Edit\u306e\u6a29\u9650\u3067\u4f5c\u6210\u3055\u308c\u305fAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002", "title": "Cloudflare\u306b\u63a5\u7d9a" }, "zone": { diff --git a/homeassistant/components/cpuspeed/translations/ja.json b/homeassistant/components/cpuspeed/translations/ja.json index e2264b57dcb..12cec097b11 100644 --- a/homeassistant/components/cpuspeed/translations/ja.json +++ b/homeassistant/components/cpuspeed/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", - "not_compatible": "CPU\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u304a\u4f7f\u3044\u306e\u30b7\u30b9\u30c6\u30e0\u3068\u4e92\u63db\u6027\u304c\u3042\u308a\u307e\u305b\u3093\u3002" + "not_compatible": "CPU\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u7d71\u5408\u306f\u3001\u304a\u4f7f\u3044\u306e\u30b7\u30b9\u30c6\u30e0\u3068\u4e92\u63db\u6027\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, "step": { "user": { diff --git a/homeassistant/components/deluge/translations/ja.json b/homeassistant/components/deluge/translations/ja.json index ab796327717..d109e1570c5 100644 --- a/homeassistant/components/deluge/translations/ja.json +++ b/homeassistant/components/deluge/translations/ja.json @@ -16,7 +16,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "web_port": "Web\u30dd\u30fc\u30c8\uff08\u8a2a\u554f\u30b5\u30fc\u30d3\u30b9\u7528\uff09" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5229\u7528\u3059\u308b\u305f\u3081\u306b\u306f\u3001deluge\u306e\u8a2d\u5b9a\u3067\u4ee5\u4e0b\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30fc\u30e2\u30f3 -> \u30ea\u30e2\u30fc\u30c8\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u306e\u8a31\u53ef" + "description": "\u3053\u306e\u7d71\u5408\u3092\u5229\u7528\u3059\u308b\u305f\u3081\u306b\u306f\u3001deluge\u306e\u8a2d\u5b9a\u3067\u4ee5\u4e0b\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30fc\u30e2\u30f3 -> \u30ea\u30e2\u30fc\u30c8\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u306e\u8a31\u53ef" } } } diff --git a/homeassistant/components/devolo_home_network/translations/ja.json b/homeassistant/components/devolo_home_network/translations/ja.json index ee08879f713..03612804d2d 100644 --- a/homeassistant/components/devolo_home_network/translations/ja.json +++ b/homeassistant/components/devolo_home_network/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002" + "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/dexcom/translations/ja.json b/homeassistant/components/dexcom/translations/ja.json index 21cc971beec..5353feeece4 100644 --- a/homeassistant/components/dexcom/translations/ja.json +++ b/homeassistant/components/dexcom/translations/ja.json @@ -16,7 +16,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "Dexcom Share\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3059\u308b", - "title": "Dexcom\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Dexcom\u7d71\u5408\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json index 199db0c326c..27e5d35e35e 100644 --- a/homeassistant/components/dialogflow/translations/ja.json +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { - "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Dialogflow\u306ewebhook\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3]({dialogflow_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Dialogflow\u306ewebhook\u7d71\u5408]({dialogflow_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 73e242dbb36..ea153d0d452 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u7d71\u5408\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discovery_error": "\u4e00\u81f4\u3059\u308bDLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "incomplete_config": "\u8a2d\u5b9a\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", diff --git a/homeassistant/components/dunehd/translations/ja.json b/homeassistant/components/dunehd/translations/ja.json index c82d7ce8f94..ebfa6f0c0ac 100644 --- a/homeassistant/components/dunehd/translations/ja.json +++ b/homeassistant/components/dunehd/translations/ja.json @@ -13,7 +13,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "Dune HD\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/elmax/translations/ja.json b/homeassistant/components/elmax/translations/ja.json index 8730ae4791b..2ae13e7a895 100644 --- a/homeassistant/components/elmax/translations/ja.json +++ b/homeassistant/components/elmax/translations/ja.json @@ -17,7 +17,7 @@ "panel_name": "\u30d1\u30cd\u30eb\u540d", "panel_pin": "PIN\u30b3\u30fc\u30c9" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u5236\u5fa1\u3059\u308b\u30d1\u30cd\u30eb\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001\u30d1\u30cd\u30eb\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308b\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u3053\u306e\u7d71\u5408\u3067\u5236\u5fa1\u3059\u308b\u30d1\u30cd\u30eb\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001\u30d1\u30cd\u30eb\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308b\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index 692b7ca8346..9fcc73679a7 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -10,7 +10,7 @@ "websocket_not_enabled": "forked-daapd server\u306eWebSocket\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002", "wrong_host_or_port": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "wrong_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", - "wrong_server_type": "forked-daapd \u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30d0\u30fc\u30b8\u30e7\u30f3 >= 27.0 \u306eforked-daapd\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" + "wrong_server_type": "forked-daapd \u306e\u7d71\u5408\u306b\u306f\u3001\u30d0\u30fc\u30b8\u30e7\u30f3 >= 27.0 \u306eforked-daapd\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" }, "flow_title": "{name} ({host})", "step": { @@ -34,7 +34,7 @@ "tts_pause_time": "TTS\u306e\u524d\u5f8c\u3067\u4e00\u6642\u505c\u6b62\u3059\u308b\u79d2\u6570", "tts_volume": "TTS\u30dc\u30ea\u30e5\u30fc\u30e0(\u7bc4\u56f2\u306f\u3001[0,1]\u306e\u5c0f\u6570\u70b9)" }, - "description": "forked-daapd\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u3055\u307e\u3056\u307e\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", + "description": "forked-daapd\u7d71\u5408\u306e\u3055\u307e\u3056\u307e\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", "title": "forked-daapd\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a" } } diff --git a/homeassistant/components/geocaching/translations/ja.json b/homeassistant/components/geocaching/translations/ja.json index 41a238aa637..5de3aba8a04 100644 --- a/homeassistant/components/geocaching/translations/ja.json +++ b/homeassistant/components/geocaching/translations/ja.json @@ -17,8 +17,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Geocaching\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Geocaching\u7d71\u5408\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/github/translations/ja.json b/homeassistant/components/github/translations/ja.json index 6b5c62e4afd..f7dfc37f64d 100644 --- a/homeassistant/components/github/translations/ja.json +++ b/homeassistant/components/github/translations/ja.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "could_not_register": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3068GitHub\u3068\u306e\u767b\u9332\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + "could_not_register": "\u7d71\u5408\u3068GitHub\u3068\u306e\u767b\u9332\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "progress": { - "wait_for_device": "1. {url} \u3092\u958b\u304f\n2. \u6b21\u306e\u30ad\u30fc\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\n```\n{code}\n```\n" + "wait_for_device": "1. {url} \u3092\u958b\u304f\n2. \u6b21\u306e\u30ad\u30fc\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u3001\u7d71\u5408\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\n```\n{code}\n```\n" }, "step": { "repositories": { diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 4cb05958a76..e18ee9c4a02 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -27,8 +27,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Nest\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index 1037d161c2b..0bafc7a3959 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU-architectuur", + "config_dir": "Configuratiemap", "dev": "Ontwikkeling", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index 8e1e35c60e2..fbac8823897 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -5,7 +5,7 @@ "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "ignored_model": "\u3053\u306e\u30e2\u30c7\u30eb\u306eHomeKit\u3067\u306e\u5bfe\u5fdc\u306f\u3001\u3088\u308a\u5b8c\u5168\u3067\u30cd\u30a4\u30c6\u30a3\u30d6\u306a\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u53ef\u80fd\u306a\u305f\u3081\u3001\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "ignored_model": "\u3053\u306e\u30e2\u30c7\u30eb\u306eHomeKit\u3067\u306e\u5bfe\u5fdc\u306f\u3001\u3088\u308a\u5b8c\u5168\u3067\u30cd\u30a4\u30c6\u30a3\u30d6\u306a\u7d71\u5408\u3092\u4f7f\u7528\u53ef\u80fd\u306a\u305f\u3081\u3001\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "invalid_config_entry": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u6e96\u5099\u304c\u3067\u304d\u3066\u3044\u308b\u3068\u8868\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant\u306b\u306f\u3059\u3067\u306b\u7af6\u5408\u3059\u308b\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308b\u305f\u3081\u3001\u5148\u306b\u3053\u308c\u3092\u524a\u9664\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "invalid_properties": "\u7121\u52b9\u306a\u30d7\u30ed\u30d1\u30c6\u30a3\u304c\u30c7\u30d0\u30a4\u30b9\u306b\u3088\u3063\u3066\u77e5\u3089\u3055\u308c\u307e\u3057\u305f\u3002", "no_devices": "\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index 4b7947b8dfa..2295e72f0b2 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -15,8 +15,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u7d71\u5408\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "trusted_device": { "data": { diff --git a/homeassistant/components/integration/translations/ja.json b/homeassistant/components/integration/translations/ja.json index 4f35ba53a4a..237d7eef353 100644 --- a/homeassistant/components/integration/translations/ja.json +++ b/homeassistant/components/integration/translations/ja.json @@ -16,7 +16,7 @@ "unit_time": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" }, "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u5408\u8a08\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u7a4d\u5206\u6642\u9593\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u30bb\u30f3\u30b5\u30fc" + "title": "\u65b0\u3057\u3044\u7d71\u5408\u30bb\u30f3\u30b5\u30fc" } } }, @@ -32,5 +32,5 @@ } } }, - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 - \u30ea\u30fc\u30de\u30f3\u548c\u7a4d\u5206\u30bb\u30f3\u30b5\u30fc" + "title": "\u7d71\u5408 - \u30ea\u30fc\u30de\u30f3\u548c\u7a4d\u5206\u30bb\u30f3\u30b5\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index f1a447901b9..60b69d07363 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -41,7 +41,7 @@ "sensor_string": "\u30ce\u30fc\u30c9 \u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217", "variable_sensor_string": "\u53ef\u5909\u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217(Variable Sensor String)" }, - "description": "ISY\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a:\n \u2022 Node Sensor String: \u540d\u524d\u306b\u3001'Node Sensor String' \u3092\u542b\u3080\u4efb\u610f\u306e\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u30d5\u30a9\u30eb\u30c0\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u307e\u305f\u306f\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u6271\u308f\u308c\u307e\u3059\u3002\n \u2022 Ignore String: \u540d\u524d\u306b\u3001'Ignore String'\u3092\u6301\u3064\u30c7\u30d0\u30a4\u30b9\u306f\u7121\u8996\u3055\u308c\u307e\u3059\u3002\n \u2022 Variable Sensor String: 'Variable Sensor String' \u3092\u542b\u3080\u5909\u6570\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u8ffd\u52a0\u3055\u308c\u307e\u3059\u3002\n \u2022 Restore Light Brightness: \u6709\u52b9\u306b\u3059\u308b\u3068\u3001\u30c7\u30d0\u30a4\u30b9\u7d44\u307f\u8fbc\u307f\u306e\u30aa\u30f3\u30ec\u30d9\u30eb\u3067\u306f\u306a\u304f\u3001\u30e9\u30a4\u30c8\u3092\u30aa\u30f3\u306b\u3057\u305f\u3068\u304d\u306b\u3001\u4ee5\u524d\u306e\u660e\u308b\u3055\u306b\u5fa9\u5143\u3055\u308c\u307e\u3059\u3002", + "description": "ISY\u7d71\u5408\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a:\n \u2022 Node Sensor String: \u540d\u524d\u306b\u3001'Node Sensor String' \u3092\u542b\u3080\u4efb\u610f\u306e\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u30d5\u30a9\u30eb\u30c0\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u307e\u305f\u306f\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u6271\u308f\u308c\u307e\u3059\u3002\n \u2022 Ignore String: \u540d\u524d\u306b\u3001'Ignore String'\u3092\u6301\u3064\u30c7\u30d0\u30a4\u30b9\u306f\u7121\u8996\u3055\u308c\u307e\u3059\u3002\n \u2022 Variable Sensor String: 'Variable Sensor String' \u3092\u542b\u3080\u5909\u6570\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u8ffd\u52a0\u3055\u308c\u307e\u3059\u3002\n \u2022 Restore Light Brightness: \u6709\u52b9\u306b\u3059\u308b\u3068\u3001\u30c7\u30d0\u30a4\u30b9\u7d44\u307f\u8fbc\u307f\u306e\u30aa\u30f3\u30ec\u30d9\u30eb\u3067\u306f\u306a\u304f\u3001\u30e9\u30a4\u30c8\u3092\u30aa\u30f3\u306b\u3057\u305f\u3068\u304d\u306b\u3001\u4ee5\u524d\u306e\u660e\u308b\u3055\u306b\u5fa9\u5143\u3055\u308c\u307e\u3059\u3002", "title": "ISY994\u30aa\u30d7\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json index 88855ff4d77..26e81ebd818 100644 --- a/homeassistant/components/kodi/translations/ja.json +++ b/homeassistant/components/kodi/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u7d71\u5408\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/laundrify/translations/ja.json b/homeassistant/components/laundrify/translations/ja.json index 153fcf99afb..020b7578e80 100644 --- a/homeassistant/components/laundrify/translations/ja.json +++ b/homeassistant/components/laundrify/translations/ja.json @@ -17,8 +17,8 @@ "description": "Laundrify-App\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308b\u3001\u3042\u306a\u305f\u306e\u500b\u4eba\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "reauth_confirm": { - "description": "Laundrify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Laundrify\u7d71\u5408\u3067\u306f\u3001\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/lg_soundbar/translations/nl.json b/homeassistant/components/lg_soundbar/translations/nl.json new file mode 100644 index 00000000000..7345479d97a --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "existing_instance_updated": "Bestaande configuratie bijgewerkt." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index 4776e3409ea..ab320748086 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -21,7 +21,7 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index cdcc937a33e..b0e54bde3c5 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -32,5 +32,12 @@ "title": "Life360-accountgegevens" } } + }, + "options": { + "step": { + "init": { + "title": "Accountinstellingen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index 98b2a11c5ff..2a5abdcb5b7 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -13,8 +13,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "(\u6b4c\u8a5e)Lyric\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "(\u6b4c\u8a5e)Lyric\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/melcloud/translations/ja.json b/homeassistant/components/melcloud/translations/ja.json index b8f2d14e5cd..377ba291786 100644 --- a/homeassistant/components/melcloud/translations/ja.json +++ b/homeassistant/components/melcloud/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3053\u306e\u30e1\u30fc\u30eb\u306b\u306f\u3059\u3067\u306b\u3001MELCloud\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002" + "already_configured": "\u3053\u306e\u30e1\u30fc\u30eb\u306b\u306f\u3059\u3067\u306b\u3001MELCloud\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/mobile_app/translations/ja.json b/homeassistant/components/mobile_app/translations/ja.json index 1f848fa1988..fa8ae167b1d 100644 --- a/homeassistant/components/mobile_app/translations/ja.json +++ b/homeassistant/components/mobile_app/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Mobile app\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u3044\u3066\u3001Home Assistant\u3068\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u3002\u4e92\u63db\u6027\u306e\u3042\u308b\u30a2\u30d7\u30ea\u306e\u4e00\u89a7\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({apps_url})\u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002" + "install_app": "Mobile app\u7d71\u5408\u3092\u958b\u3044\u3066\u3001Home Assistant\u3068\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u3002\u4e92\u63db\u6027\u306e\u3042\u308b\u30a2\u30d7\u30ea\u306e\u4e00\u89a7\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({apps_url})\u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index a5b9df933ef..35ad69b3e1c 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -10,14 +10,14 @@ }, "step": { "usb_confirm": { - "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u7d71\u5408\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" }, "user": { "data": { "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8" }, - "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u7d71\u5408\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/mullvad/translations/ja.json b/homeassistant/components/mullvad/translations/ja.json index fc1791527f9..3119597977a 100644 --- a/homeassistant/components/mullvad/translations/ja.json +++ b/homeassistant/components/mullvad/translations/ja.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Mullvad VPN\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + "description": "Mullvad VPN\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } } diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index da0354fe3c7..4a73363bf16 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -33,7 +33,7 @@ "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", "invalid_subscribe_topic": "\u7121\u52b9\u306a\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6 \u30c8\u30d4\u30c3\u30af", "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", - "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "mqtt_required": "MQTT\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "same_topic": "\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6\u3068\u30d1\u30d6\u30ea\u30c3\u30b7\u30e5\u306e\u30c8\u30d4\u30c3\u30af\u304c\u540c\u3058\u3067\u3059", diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index 2125c9e3e38..d5147344c61 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -34,7 +34,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Nettigo Air Monitor\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "description": "Nettigo Air Monitor\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 9aff487997c..398b09f92c1 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -77,8 +77,8 @@ "title": "Google Cloud\u306e\u8a2d\u5b9a" }, "reauth_confirm": { - "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Nest\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index b9f2f17960a..016411aef41 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -15,8 +15,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Netatmo\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/nextdns/translations/de.json b/homeassistant/components/nextdns/translations/de.json index 58e470e06ff..51a5eaf5edd 100644 --- a/homeassistant/components/nextdns/translations/de.json +++ b/homeassistant/components/nextdns/translations/de.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Server erreichen" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/et.json b/homeassistant/components/nextdns/translations/et.json index 9b4d56f51d4..eaf8efc0341 100644 --- a/homeassistant/components/nextdns/translations/et.json +++ b/homeassistant/components/nextdns/translations/et.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "\u00dchendu serveriga" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ja.json b/homeassistant/components/nextdns/translations/ja.json index 7c1f0614fc1..df57113cc58 100644 --- a/homeassistant/components/nextdns/translations/ja.json +++ b/homeassistant/components/nextdns/translations/ja.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/nl.json b/homeassistant/components/nextdns/translations/nl.json new file mode 100644 index 00000000000..436e1a68b7d --- /dev/null +++ b/homeassistant/components/nextdns/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel", + "unknown": "Onverwachte fout" + }, + "step": { + "profiles": { + "data": { + "profile": "Profiel" + } + }, + "user": { + "data": { + "api_key": "API-sleutel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index fff28117234..3db6768efcb 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -13,7 +13,7 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002" + "description": "\u3053\u306e\u7d71\u5408\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json index 8cd2a58e1e0..2171d2722a1 100644 --- a/homeassistant/components/notion/translations/ja.json +++ b/homeassistant/components/notion/translations/ja.json @@ -14,7 +14,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5ea6\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/nuki/translations/ja.json b/homeassistant/components/nuki/translations/ja.json index 6f54d0d8b4b..d4e1d68d780 100644 --- a/homeassistant/components/nuki/translations/ja.json +++ b/homeassistant/components/nuki/translations/ja.json @@ -13,8 +13,8 @@ "data": { "token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, - "description": "Nuki\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001bridge\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Nuki\u7d71\u5408\u3067\u306f\u3001bridge\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index 511d59b2138..b89b1ef0985 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -15,9 +15,9 @@ "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", "mode": "\u30e2\u30fc\u30c9", - "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" + "name": "\u540d\u524d" }, - "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "OpenWeatherMap\u306e\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json index fd9fe67db73..b4f73a91406 100644 --- a/homeassistant/components/picnic/translations/ja.json +++ b/homeassistant/components/picnic/translations/ja.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "different_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a\u3067\u4f7f\u7528\u3057\u305f\u3082\u306e\u3068\u540c\u3058\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "different_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u7d71\u5408\u306e\u8a2d\u5b9a\u3067\u4f7f\u7528\u3057\u305f\u3082\u306e\u3068\u540c\u3058\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index 87b8d501e0d..0103774c138 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "invalid_setup": "Anna\u306e\u4ee3\u308f\u308a\u306b\u3001Adam\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant Plugwise\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_setup": "Anna\u306e\u4ee3\u308f\u308a\u306b\u3001Adam\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant Plugwise\u7d71\u5408\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", diff --git a/homeassistant/components/qnap_qsw/translations/nl.json b/homeassistant/components/qnap_qsw/translations/nl.json index 10ec94f589d..73e0af1fe00 100644 --- a/homeassistant/components/qnap_qsw/translations/nl.json +++ b/homeassistant/components/qnap_qsw/translations/nl.json @@ -9,6 +9,12 @@ "invalid_auth": "Ongeldige authenticatie" }, "step": { + "discovered_connection": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/renault/translations/ja.json b/homeassistant/components/renault/translations/ja.json index 743dfa36fb5..42d819589b7 100644 --- a/homeassistant/components/renault/translations/ja.json +++ b/homeassistant/components/renault/translations/ja.json @@ -20,7 +20,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/ridwell/translations/ja.json b/homeassistant/components/ridwell/translations/ja.json index 33d7e12ebd4..746d237457f 100644 --- a/homeassistant/components/ridwell/translations/ja.json +++ b/homeassistant/components/ridwell/translations/ja.json @@ -14,7 +14,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/ja.json b/homeassistant/components/rtsp_to_webrtc/translations/ja.json index 6359d66f50a..c904164a599 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/ja.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/ja.json @@ -19,7 +19,7 @@ "data": { "server_url": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL \u4f8b: https://example.com" }, - "description": "RTSPtoWebRTC\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092WebRTC\u306b\u5909\u63db\u3059\u308b\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "description": "RTSPtoWebRTC\u7d71\u5408\u306f\u3001RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092WebRTC\u306b\u5909\u63db\u3059\u308b\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "RTSPtoWebRTC\u306e\u8a2d\u5b9a" } } diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json index 437ce96d9f1..50dbc070ac9 100644 --- a/homeassistant/components/sense/translations/ja.json +++ b/homeassistant/components/sense/translations/ja.json @@ -14,8 +14,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "Sense\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {email} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Sense\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {email} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index ee691ac3901..176318b3f3c 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, "error": { "cannot_connect": "Kan geen verbinding maken" }, diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 57909d30f69..e264ec18aab 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -18,7 +18,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30a2\u30af\u30bb\u30b9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u53d6\u308a\u6d88\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u30ea\u30f3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "sms_2fa": { "data": { diff --git a/homeassistant/components/sleepiq/translations/ja.json b/homeassistant/components/sleepiq/translations/ja.json index b1e0ec1d86e..fb81857cb5f 100644 --- a/homeassistant/components/sleepiq/translations/ja.json +++ b/homeassistant/components/sleepiq/translations/ja.json @@ -13,8 +13,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "SleepIQ\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {username} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "SleepIQ\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {username} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json index 9dff009e3dd..aabbfd52564 100644 --- a/homeassistant/components/smappee/translations/ja.json +++ b/homeassistant/components/smappee/translations/ja.json @@ -5,7 +5,7 @@ "already_configured_local_device": "\u30ed\u30fc\u30ab\u30eb\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30af\u30e9\u30a6\u30c9\u30c7\u30d0\u30a4\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u524d\u306b\u3001\u307e\u305a\u305d\u308c\u3089\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_mdns": "Smappee\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3002", + "invalid_mdns": "Smappee\u7d71\u5408\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" }, @@ -21,7 +21,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30db\u30b9\u30c8\u3092\u5165\u529b\u3057\u3066\u3001Smappee local\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u59cb\u3057\u307e\u3059" + "description": "\u30db\u30b9\u30c8\u3092\u5165\u529b\u3057\u3066\u3001Smappee local\u7d71\u5408\u3092\u958b\u59cb\u3057\u307e\u3059" }, "pick_implementation": { "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index e522744cd4d..94d1aba2606 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -19,14 +19,14 @@ "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, - "description": "[\u624b\u9806]({component_url})\u3054\u3068\u306b\u4f5c\u6210\u3055\u308c\u305f\u3001SmartThings[\u500b\u4eba\u7528\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3]({token_url})\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u3053\u308c\u306f\u3001SmartThings account\u5185\u306bHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f5c\u6210\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", + "description": "[\u624b\u9806]({component_url})\u3054\u3068\u306b\u4f5c\u6210\u3055\u308c\u305f\u3001SmartThings[\u500b\u4eba\u7528\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3]({token_url})\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u3053\u308c\u306f\u3001SmartThings account\u5185\u306bHome Assistant\u7d71\u5408\u3092\u4f5c\u6210\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", "title": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "select_location": { "data": { "location_id": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" }, - "description": "Home Assistant\u306b\u8ffd\u52a0\u3057\u305f\u3044SmartThings\u306e\u5834\u6240\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3059\u308b\u3068\u3001\u65b0\u3057\u3044\u30a6\u30a3\u30f3\u30c9\u30a6\u304c\u958b\u304f\u306e\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u9078\u629e\u3057\u305f\u5834\u6240\u3078\u306eHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u627f\u8a8d\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002", + "description": "Home Assistant\u306b\u8ffd\u52a0\u3057\u305f\u3044SmartThings\u306e\u5834\u6240\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3059\u308b\u3068\u3001\u65b0\u3057\u3044\u30a6\u30a3\u30f3\u30c9\u30a6\u304c\u958b\u304f\u306e\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u9078\u629e\u3057\u305f\u5834\u6240\u3078\u306eHome Assistant\u7d71\u5408\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u627f\u8a8d\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002", "title": "\u5834\u6240\u3092\u9078\u629e" }, "user": { diff --git a/homeassistant/components/smarttub/translations/ja.json b/homeassistant/components/smarttub/translations/ja.json index 44d753fa22e..1488ddf9e16 100644 --- a/homeassistant/components/smarttub/translations/ja.json +++ b/homeassistant/components/smarttub/translations/ja.json @@ -9,8 +9,8 @@ }, "step": { "reauth_confirm": { - "description": "SmartTub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "SmartTub\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index 49f819659b4..97b8e990af7 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -16,7 +16,7 @@ "port": "\u30dd\u30fc\u30c8", "system_id": "\u30b7\u30b9\u30c6\u30e0ID" }, - "description": "\u30b7\u30b9\u30c6\u30e0ID \u306f\u3001\u30af\u30e9\u30a6\u30c9\u4ee5\u5916\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u9078\u629e\u3059\u308b\u3053\u3068\u306b\u3088\u308a\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e MyLink \u30a2\u30d7\u30ea\u3067\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002" + "description": "\u30b7\u30b9\u30c6\u30e0ID \u306f\u3001\u30af\u30e9\u30a6\u30c9\u4ee5\u5916\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u9078\u629e\u3059\u308b\u3053\u3068\u306b\u3088\u308a\u3001\u7d71\u5408\u306e MyLink \u30a2\u30d7\u30ea\u3067\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index cbe230bbc94..d37c4915481 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -12,8 +12,8 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u30db\u30b9\u30c8\u3055\u308c\u3066\u3044\u308bSonarr API\u3067\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {host}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Sonarr\u7d71\u5408\u306f\u3001\u30db\u30b9\u30c8\u3055\u308c\u3066\u3044\u308bSonarr API\u3067\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {host}", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/soundtouch/translations/nl.json b/homeassistant/components/soundtouch/translations/nl.json new file mode 100644 index 00000000000..0ccc8057ac8 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index 9c0e308dc18..b0b94ebe5ab 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "missing_configuration": "Spotify\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_account_mismatch": "\u8a8d\u8a3c\u3055\u308c\u305fSpotify\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u3001\u518d\u8a8d\u8a3c\u304c\u5fc5\u8981\u306a\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, @@ -14,8 +14,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Spotify\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {account}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Spotify\u7d71\u5408\u3067\u306f\u3001Spotify\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {account}", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index e138d46319b..f62fd45e767 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -12,8 +12,8 @@ }, "step": { "reauth_confirm": { - "description": "Steam\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\n\n\u3053\u3053\u3067\u3042\u306a\u305f\u306e\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059: {api_key_url}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Steam\u7d71\u5408\u306f\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\n\n\u3053\u3053\u3067\u3042\u306a\u305f\u306e\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059: {api_key_url}", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/syncthing/translations/ja.json b/homeassistant/components/syncthing/translations/ja.json index 2a725cbf3cc..ee9c4b02e26 100644 --- a/homeassistant/components/syncthing/translations/ja.json +++ b/homeassistant/components/syncthing/translations/ja.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "title": "Syncthing\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", + "title": "Syncthing\u7d71\u5408\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", "token": "\u30c8\u30fc\u30af\u30f3", "url": "URL", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 47245b2ceb8..2654c5f3910 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -35,7 +35,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "title": "Synology DSM \u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "Synology DSM \u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index 1e7750b2442..65f56bffc68 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -19,7 +19,7 @@ }, "reauth_confirm": { "description": "Total Connect\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/tractive/translations/ja.json b/homeassistant/components/tractive/translations/ja.json index 7f97d4c23f4..97defadf3ff 100644 --- a/homeassistant/components/tractive/translations/ja.json +++ b/homeassistant/components/tractive/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 6f3b3191c83..f12b734a7be 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -15,7 +15,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json index 81bdabdc8d3..babf5963357 100644 --- a/homeassistant/components/ukraine_alarm/translations/ja.json +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -25,7 +25,7 @@ "data": { "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, - "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index fba092385c1..cd810e12d6a 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -27,7 +27,7 @@ }, "options": { "abort": { - "integration_not_setup": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "integration_not_setup": "UniFi\u7d71\u5408\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3066\u3044\u307e\u305b\u3093" }, "step": { "client_control": { @@ -57,7 +57,7 @@ "track_clients": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308b", "track_devices": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1(\u30e6\u30d3\u30ad\u30c6\u30a3\u30c7\u30d0\u30a4\u30b9)" }, - "description": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" + "description": "UniFi\u7d71\u5408\u306e\u8a2d\u5b9a" }, "statistics_sensors": { "data": { diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index e8c66b5088b..55ff808725f 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -19,7 +19,7 @@ "api_key": "API\u30ad\u30fc" }, "description": "UptimeRobot\u304b\u3089\u65b0\u898f\u306e\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/vicare/translations/ja.json b/homeassistant/components/vicare/translations/ja.json index aa7e159778c..10ffe077aca 100644 --- a/homeassistant/components/vicare/translations/ja.json +++ b/homeassistant/components/vicare/translations/ja.json @@ -16,7 +16,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" }, - "description": "ViCare\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.viessmann.com \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "ViCare\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.viessmann.com \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/vulcan/translations/ja.json b/homeassistant/components/vulcan/translations/ja.json index 4f18d47b7ea..5d6f5bbcca7 100644 --- a/homeassistant/components/vulcan/translations/ja.json +++ b/homeassistant/components/vulcan/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "all_student_already_configured": "\u3059\u3079\u3066\u306e\u751f\u5f92\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_configured": "\u305d\u306e\u5b66\u751f\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", - "no_matching_entries": "\u4e00\u81f4\u3059\u308b\u30a8\u30f3\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u304b\u3001outdated student\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044..", + "no_matching_entries": "\u4e00\u81f4\u3059\u308b\u30a8\u30f3\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u304b\u3001outdated student\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044..", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f" }, "error": { @@ -48,7 +48,7 @@ "data": { "student_name": "\u751f\u5f92\u3092\u9078\u629e(Select student)" }, - "description": "\u751f\u5f92\u3092\u9078\u629e\u3057\u307e\u3059(Select student)\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3067\u3001\u3088\u308a\u591a\u304f\u306e\u751f\u5f92\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" + "description": "\u751f\u5f92\u3092\u9078\u629e\u3057\u307e\u3059(Select student)\u3002\u7d71\u5408\u3092\u518d\u5ea6\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3067\u3001\u3088\u308a\u591a\u304f\u306e\u751f\u5f92\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/watttime/translations/ja.json b/homeassistant/components/watttime/translations/ja.json index 87b71e0675f..93d36406019 100644 --- a/homeassistant/components/watttime/translations/ja.json +++ b/homeassistant/components/watttime/translations/ja.json @@ -28,7 +28,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index 545c0cdb0d6..3463699509a 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -31,7 +31,7 @@ "units": "\u5358\u4f4d", "vehicle_type": "\u8eca\u4e21\u30bf\u30a4\u30d7" }, - "description": "`substring`\u30a4\u30f3\u30d7\u30c3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3088\u3046\u306b\u5f37\u5236\u3057\u305f\u308a\u3001\u9006\u306b\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u56de\u907f\u3057\u305f\u30bf\u30a4\u30e0\u30c8\u30e9\u30d9\u30eb\u306e\u8a08\u7b97\u3092\u884c\u3046\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" + "description": "`substring`\u30a4\u30f3\u30d7\u30c3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u7d71\u5408\u3067\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3088\u3046\u306b\u5f37\u5236\u3057\u305f\u308a\u3001\u9006\u306b\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u56de\u907f\u3057\u305f\u30bf\u30a4\u30e0\u30c8\u30e9\u30d9\u30eb\u306e\u8a08\u7b97\u3092\u884c\u3046\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/whois/translations/ja.json b/homeassistant/components/whois/translations/ja.json index 66a7868a265..132ebf9e070 100644 --- a/homeassistant/components/whois/translations/ja.json +++ b/homeassistant/components/whois/translations/ja.json @@ -6,7 +6,7 @@ "error": { "unexpected_response": "Whois\u30b5\u30fc\u30d0\u30fc\u304b\u3089\u306e\u4e88\u671f\u3057\u306a\u3044\u5fdc\u7b54", "unknown_date_format": "Whois\u30b5\u30fc\u30d0\u30fc\u306e\u5fdc\u7b54\u3067\u4e0d\u660e\u306a\u65e5\u4ed8\u30d5\u30a9\u30fc\u30de\u30c3\u30c8", - "unknown_tld": "\u6307\u5b9a\u3055\u308c\u305fTLD\u306f\u4e0d\u660e\u3001\u3082\u3057\u304f\u306f\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093", + "unknown_tld": "\u6307\u5b9a\u3055\u308c\u305fTLD\u306f\u4e0d\u660e\u3001\u3082\u3057\u304f\u306f\u3053\u306e\u7d71\u5408\u3067\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093", "whois_command_failed": "Whois\u30b3\u30de\u30f3\u30c9\u304c\u5931\u6557\u3057\u307e\u3057\u305f: whois\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "step": { diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index bc70bf0c746..20f097e973c 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -26,7 +26,7 @@ }, "reauth": { "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/wiz/translations/ja.json b/homeassistant/components/wiz/translations/ja.json index e062fbdb75b..b8e1536654b 100644 --- a/homeassistant/components/wiz/translations/ja.json +++ b/homeassistant/components/wiz/translations/ja.json @@ -9,7 +9,7 @@ "bulb_time_out": "\u96fb\u7403\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u96fb\u7403\u304c\u30aa\u30d5\u30e9\u30a4\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u9593\u9055\u3063\u305fIP/\u30db\u30b9\u30c8\u304c\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u96fb\u7403\u306e\u96fb\u6e90\u3092\u5165\u308c\u3066\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_ip": "\u6709\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", - "no_wiz_light": "\u3053\u306e\u96fb\u7403\u306f\u3001WiZ Platform\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "no_wiz_light": "\u3053\u306e\u96fb\u7403\u306f\u3001WiZ Platform\u7d71\u5408\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index 5f30617d7eb..af3654ea2b0 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "cct_unsupported": "\u3053\u306eWLED\u30c7\u30d0\u30a4\u30b9\u306fCCT\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "cct_unsupported": "\u3053\u306eWLED\u30c7\u30d0\u30a4\u30b9\u306fCCT\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 0877850f754..672a338f082 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -36,11 +36,11 @@ "host": "IP\u30a2\u30c9\u30ec\u30b9", "token": "API\u30c8\u30fc\u30af\u30f3" }, - "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002" + "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u7d71\u5408\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002" }, "reauth_confirm": { - "description": "Xiaomi Miio\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Xiaomi Miio\u7d71\u5408\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "select": { "data": { diff --git a/homeassistant/components/yolink/translations/ja.json b/homeassistant/components/yolink/translations/ja.json index 7d2545803bf..274296a1240 100644 --- a/homeassistant/components/yolink/translations/ja.json +++ b/homeassistant/components/yolink/translations/ja.json @@ -17,8 +17,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Yolink\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Yolink\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index f4c5a62b050..94645e302f4 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -36,7 +36,7 @@ "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b" }, "hassio_confirm": { - "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "install_addon": { "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f\u3002" From 0e3f7bc63aecc71685536f2121d3fecbc24bd642 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 8 Jul 2022 05:49:07 +0200 Subject: [PATCH 211/820] Resolution center MVP (#74243) Co-authored-by: Paulus Schoutsen --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/resolution_center/__init__.py | 20 + .../components/resolution_center/const.py | 3 + .../resolution_center/issue_handler.py | 62 ++++ .../resolution_center/issue_registry.py | 164 +++++++++ .../resolution_center/manifest.json | 7 + .../components/resolution_center/models.py | 12 + .../resolution_center/websocket_api.py | 62 ++++ mypy.ini | 11 + script/hassfest/manifest.py | 1 + .../components/resolution_center/__init__.py | 1 + .../components/resolution_center/test_init.py | 346 ++++++++++++++++++ .../resolution_center/test_issue_registry.py | 92 +++++ .../resolution_center/test_websocket_api.py | 151 ++++++++ 15 files changed, 935 insertions(+) create mode 100644 homeassistant/components/resolution_center/__init__.py create mode 100644 homeassistant/components/resolution_center/const.py create mode 100644 homeassistant/components/resolution_center/issue_handler.py create mode 100644 homeassistant/components/resolution_center/issue_registry.py create mode 100644 homeassistant/components/resolution_center/manifest.json create mode 100644 homeassistant/components/resolution_center/models.py create mode 100644 homeassistant/components/resolution_center/websocket_api.py create mode 100644 tests/components/resolution_center/__init__.py create mode 100644 tests/components/resolution_center/test_init.py create mode 100644 tests/components/resolution_center/test_issue_registry.py create mode 100644 tests/components/resolution_center/test_websocket_api.py diff --git a/.strict-typing b/.strict-typing index ebfed5dfa5b..c5ed000a7ce 100644 --- a/.strict-typing +++ b/.strict-typing @@ -191,6 +191,7 @@ homeassistant.components.recollect_waste.* homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* +homeassistant.components.resolution_center.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* diff --git a/CODEOWNERS b/CODEOWNERS index 9f53aeab34e..fda94805214 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -857,6 +857,8 @@ build.json @home-assistant/supervisor /homeassistant/components/renault/ @epenet /tests/components/renault/ @epenet /homeassistant/components/repetier/ @MTrab @ShadowBr0ther +/homeassistant/components/resolution_center/ @home-assistant/core +/tests/components/resolution_center/ @home-assistant/core /homeassistant/components/rflink/ @javicalle /tests/components/rflink/ @javicalle /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/resolution_center/__init__.py b/homeassistant/components/resolution_center/__init__.py new file mode 100644 index 00000000000..1446aa68bba --- /dev/null +++ b/homeassistant/components/resolution_center/__init__.py @@ -0,0 +1,20 @@ +"""The resolution center integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from . import websocket_api +from .const import DOMAIN +from .issue_handler import async_create_issue, async_delete_issue +from .issue_registry import async_load as async_load_issue_registry + +__all__ = ["DOMAIN", "async_create_issue", "async_delete_issue"] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Resolution Center.""" + websocket_api.async_setup(hass) + await async_load_issue_registry(hass) + + return True diff --git a/homeassistant/components/resolution_center/const.py b/homeassistant/components/resolution_center/const.py new file mode 100644 index 00000000000..46d020e5118 --- /dev/null +++ b/homeassistant/components/resolution_center/const.py @@ -0,0 +1,3 @@ +"""Constants for the Resolution Center integration.""" + +DOMAIN = "resolution_center" diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/resolution_center/issue_handler.py new file mode 100644 index 00000000000..245895fa2db --- /dev/null +++ b/homeassistant/components/resolution_center/issue_handler.py @@ -0,0 +1,62 @@ +"""The resolution center integration.""" +from __future__ import annotations + +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.core import HomeAssistant, callback + +from .issue_registry import async_get as async_get_issue_registry +from .models import IssueSeverity + + +@callback +def async_create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + # Verify the breaks_in_ha_version is a valid version string + if breaks_in_ha_version: + AwesomeVersion( + breaks_in_ha_version, + ensure_strategy=AwesomeVersionStrategy.CALVER, + find_first_match=False, + ) + + issue_registry = async_get_issue_registry(hass) + issue_registry.async_get_or_create( + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + +@callback +def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + issue_registry = async_get_issue_registry(hass) + issue_registry.async_delete(domain, issue_id) + + +@callback +def async_dismiss_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Dismiss an issue. + + Will raise if the issue does not exist. + """ + issue_registry = async_get_issue_registry(hass) + issue_registry.async_dismiss(domain, issue_id) diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py new file mode 100644 index 00000000000..d97ad73bbac --- /dev/null +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -0,0 +1,164 @@ +"""Persistently store issues raised by integrations.""" +from __future__ import annotations + +import dataclasses +from typing import cast + +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store + +from .models import IssueSeverity + +DATA_REGISTRY = "issue_registry" +STORAGE_KEY = "resolution_center.issue_registry" +STORAGE_VERSION = 1 +SAVE_DELAY = 10 +SAVED_FIELDS = ("dismissed_version", "domain", "issue_id") + + +@dataclasses.dataclass(frozen=True) +class IssueEntry: + """Issue Registry Entry.""" + + active: bool + breaks_in_ha_version: str | None + dismissed_version: str | None + domain: str + issue_id: str + learn_more_url: str | None + severity: IssueSeverity | None + translation_key: str | None + translation_placeholders: dict[str, str] | None + + +class IssueRegistry: + """Class to hold a registry of issues.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the issue registry.""" + self.hass = hass + self.issues: dict[tuple[str, str], IssueEntry] = {} + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) + + @callback + def async_get_issue(self, domain: str, issue_id: str) -> IssueEntry | None: + """Get issue by id.""" + return self.issues.get((domain, issue_id)) + + @callback + def async_get_or_create( + self, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, + ) -> IssueEntry: + """Get issue. Create if it doesn't exist.""" + + if (issue := self.async_get_issue(domain, issue_id)) is None: + issue = IssueEntry( + active=True, + breaks_in_ha_version=breaks_in_ha_version, + dismissed_version=None, + domain=domain, + issue_id=issue_id, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self.issues[(domain, issue_id)] = issue + self.async_schedule_save() + else: + issue = self.issues[(domain, issue_id)] = dataclasses.replace( + issue, + active=True, + breaks_in_ha_version=breaks_in_ha_version, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + return issue + + @callback + def async_delete(self, domain: str, issue_id: str) -> None: + """Delete issue.""" + if self.issues.pop((domain, issue_id), None) is None: + return + + self.async_schedule_save() + + @callback + def async_dismiss(self, domain: str, issue_id: str) -> IssueEntry: + """Dismiss issue.""" + old = self.issues[(domain, issue_id)] + if old.dismissed_version == ha_version: + return old + + issue = self.issues[(domain, issue_id)] = dataclasses.replace( + old, + dismissed_version=ha_version, + ) + + self.async_schedule_save() + + return issue + + async def async_load(self) -> None: + """Load the issue registry.""" + data = await self._store.async_load() + + issues: dict[tuple[str, str], IssueEntry] = {} + + if isinstance(data, dict): + for issue in data["issues"]: + issues[(issue["domain"], issue["issue_id"])] = IssueEntry( + active=False, + breaks_in_ha_version=None, + dismissed_version=issue["dismissed_version"], + domain=issue["domain"], + issue_id=issue["issue_id"], + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) + + self.issues = issues + + @callback + def async_schedule_save(self) -> None: + """Schedule saving the issue registry.""" + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: + """Return data of issue registry to store in a file.""" + data = {} + + data["issues"] = [ + {field: getattr(entry, field) for field in SAVED_FIELDS} + for entry in self.issues.values() + ] + + return data + + +@callback +def async_get(hass: HomeAssistant) -> IssueRegistry: + """Get issue registry.""" + return cast(IssueRegistry, hass.data[DATA_REGISTRY]) + + +async def async_load(hass: HomeAssistant) -> None: + """Load issue registry.""" + assert DATA_REGISTRY not in hass.data + hass.data[DATA_REGISTRY] = IssueRegistry(hass) + await hass.data[DATA_REGISTRY].async_load() diff --git a/homeassistant/components/resolution_center/manifest.json b/homeassistant/components/resolution_center/manifest.json new file mode 100644 index 00000000000..87cd309ad3d --- /dev/null +++ b/homeassistant/components/resolution_center/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "resolution_center", + "name": "Resolution Center", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/resolution_center", + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/components/resolution_center/models.py b/homeassistant/components/resolution_center/models.py new file mode 100644 index 00000000000..eabfd98cef3 --- /dev/null +++ b/homeassistant/components/resolution_center/models.py @@ -0,0 +1,12 @@ +"""Models for Resolution Center.""" +from __future__ import annotations + +from homeassistant.backports.enum import StrEnum + + +class IssueSeverity(StrEnum): + """Issue severity.""" + + CRITICAL = "critical" + ERROR = "error" + WARNING = "warning" diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py new file mode 100644 index 00000000000..14793f0bd2d --- /dev/null +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -0,0 +1,62 @@ +"""The resolution center websocket API.""" +from __future__ import annotations + +import dataclasses +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .issue_handler import async_dismiss_issue +from .issue_registry import async_get as async_get_issue_registry + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the resolution center websocket API.""" + websocket_api.async_register_command(hass, ws_dismiss_issue) + websocket_api.async_register_command(hass, ws_list_issues) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "resolution_center/dismiss_issue", + vol.Required("domain"): str, + vol.Required("issue_id"): str, + } +) +def ws_dismiss_issue( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Fix an issue.""" + async_dismiss_issue(hass, msg["domain"], msg["issue_id"]) + + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "resolution_center/list_issues", + } +) +@callback +def ws_list_issues( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Return a list of issues.""" + + def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: + result = {k: v for k, v in kv_pairs if k != "active"} + result["dismissed"] = result["dismissed_version"] is not None + return result + + issue_registry = async_get_issue_registry(hass) + issues = [ + dataclasses.asdict(issue, dict_factory=ws_dict) + for issue in issue_registry.issues.values() + ] + + connection.send_result(msg["id"], {"issues": issues}) diff --git a/mypy.ini b/mypy.ini index 87dd265eeef..c47413b4af8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1864,6 +1864,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.resolution_center.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ridwell.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 0cd20364533..b847d384361 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -80,6 +80,7 @@ NO_IOT_CLASS = [ "proxy", "python_script", "raspberry_pi", + "resolution_center", "safe_mode", "script", "search", diff --git a/tests/components/resolution_center/__init__.py b/tests/components/resolution_center/__init__.py new file mode 100644 index 00000000000..a6a86bf99bc --- /dev/null +++ b/tests/components/resolution_center/__init__.py @@ -0,0 +1 @@ +"""Tests for the resolution center integration.""" diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py new file mode 100644 index 00000000000..869c5d6c485 --- /dev/null +++ b/tests/components/resolution_center/test_init.py @@ -0,0 +1,346 @@ +"""Test the resolution center websocket API.""" +import pytest + +from homeassistant.components.resolution_center import ( + async_create_issue, + async_delete_issue, +) +from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test creating and updating issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9.0dev0", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Update an issue + async_create_issue( + hass, + issues[0]["domain"], + issues[0]["issue_id"], + breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + learn_more_url="blablabla", + severity=issues[0]["severity"], + translation_key=issues[0]["translation_key"], + translation_placeholders=issues[0]["translation_placeholders"], + ) + + await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["issues"][0] == dict( + issues[0], + dismissed=False, + dismissed_version=None, + learn_more_url="blablabla", + ) + + +@pytest.mark.parametrize("ha_version", ("2022.9.cat", "In the future: 2023.1.1")) +async def test_create_issue_invalid_version( + hass: HomeAssistant, hass_ws_client, ha_version +) -> None: + """Test creating an issue with invalid breaks in version.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issue = { + "breaks_in_ha_version": ha_version, + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + } + + with pytest.raises(Exception): + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test dismissing issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Dismiss a non-existing issue + with pytest.raises(KeyError): + async_dismiss_issue(hass, issues[0]["domain"], "no_such_issue") + + await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Dismiss an existing issue + async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=True, + dismissed_version=ha_version, + ) + for issue in issues + ] + } + + # Dismiss the same issue again + async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=True, + dismissed_version=ha_version, + ) + for issue in issues + ] + } + + # Update a dismissed issue + async_create_issue( + hass, + issues[0]["domain"], + issues[0]["issue_id"], + breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + learn_more_url="blablabla", + severity=issues[0]["severity"], + translation_key=issues[0]["translation_key"], + translation_placeholders=issues[0]["translation_placeholders"], + ) + + await client.send_json({"id": 6, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["issues"][0] == dict( + issues[0], + dismissed=True, + dismissed_version=ha_version, + learn_more_url="blablabla", + ) + + +async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can delete an issue.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "fake_integration", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Delete a non-existing issue + async_delete_issue(hass, issues[0]["domain"], "no_such_issue") + + await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Delete an existing issue + async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + # Delete the same issue again + async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py new file mode 100644 index 00000000000..c236e96adb2 --- /dev/null +++ b/tests/components/resolution_center/test_issue_registry.py @@ -0,0 +1,92 @@ +"""Test the resolution center websocket API.""" +from homeassistant.components.resolution_center import ( + async_create_issue, + issue_registry, +) +from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import flush_store + + +async def test_load_issues(hass: HomeAssistant) -> None: + """Make sure that we can load/save data correctly.""" + assert await async_setup_component(hass, DOMAIN, {}) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 2 + issue1 = registry.async_get_issue("test", "issue_1") + issue2 = registry.async_get_issue("test", "issue_2") + + registry2 = issue_registry.IssueRegistry(hass) + await flush_store(registry._store) + await registry2.async_load() + + assert list(registry.issues) == list(registry2.issues) + + issue1_registry2 = registry2.async_get_issue("test", "issue_1") + assert issue1_registry2.dismissed_version == issue1.dismissed_version + issue2_registry2 = registry2.async_get_issue("test", "issue_2") + assert issue2_registry2.dismissed_version == issue2.dismissed_version + + +async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> None: + """Test loading stored issues on start.""" + hass_storage[issue_registry.STORAGE_KEY] = { + "version": issue_registry.STORAGE_VERSION, + "data": { + "issues": [ + { + "dismissed_version": "2022.7.0.dev0", + "domain": "test", + "issue_id": "issue_1", + }, + { + "dismissed_version": None, + "domain": "test", + "issue_id": "issue_2", + }, + ] + }, + } + + assert await async_setup_component(hass, DOMAIN, {}) + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 2 diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py new file mode 100644 index 00000000000..9258a06f904 --- /dev/null +++ b/tests/components/resolution_center/test_websocket_api.py @@ -0,0 +1,151 @@ +"""Test the resolution center websocket API.""" +from homeassistant.components.resolution_center import async_create_issue +from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can dismiss an issue.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + await client.send_json( + { + "id": 2, + "type": "resolution_center/dismiss_issue", + "domain": "test", + "issue_id": "no_such_issue", + } + ) + msg = await client.receive_json() + assert not msg["success"] + + await client.send_json( + { + "id": 3, + "type": "resolution_center/dismiss_issue", + "domain": "test", + "issue_id": "issue_1", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=True, + dismissed_version=ha_version, + ) + for issue in issues + ] + } + + +async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can list issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } From 561c9a77d87e15e81367556996d54f7b0332f704 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 7 Jul 2022 20:50:19 -0700 Subject: [PATCH 212/820] Add google calendar service proper exception handling (#74686) --- homeassistant/components/google/__init__.py | 27 ++++++++++++------- homeassistant/components/google/calendar.py | 23 +++++++++------- tests/components/google/conftest.py | 5 +++- tests/components/google/test_init.py | 29 +++++++++++++++++++++ 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 5553350aa23..261c61d1a88 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -30,7 +30,11 @@ from homeassistant.const import ( CONF_OFFSET, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -348,15 +352,18 @@ async def async_setup_add_event_service( "Missing required fields to set start or end date/datetime" ) - await calendar_service.async_create_event( - call.data[EVENT_CALENDAR_ID], - Event( - summary=call.data[EVENT_SUMMARY], - description=call.data[EVENT_DESCRIPTION], - start=start, - end=end, - ), - ) + try: + await calendar_service.async_create_event( + call.data[EVENT_CALENDAR_ID], + Event( + summary=call.data[EVENT_SUMMARY], + description=call.data[EVENT_DESCRIPTION], + start=start, + end=end, + ), + ) + except ApiException as err: + raise HomeAssistantError(str(err)) from err hass.services.async_register( DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 3c271a2c3c3..a86cdf55e3a 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -22,7 +22,7 @@ from homeassistant.components.calendar import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import PlatformNotReady +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import ( config_validation as cv, entity_platform, @@ -362,12 +362,15 @@ async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) -> if start is None or end is None: raise ValueError("Missing required fields to set start or end date/datetime") - await entity.calendar_service.async_create_event( - entity.calendar_id, - Event( - summary=call.data[EVENT_SUMMARY], - description=call.data[EVENT_DESCRIPTION], - start=start, - end=end, - ), - ) + try: + await entity.calendar_service.async_create_event( + entity.calendar_id, + Event( + summary=call.data[EVENT_SUMMARY], + description=call.data[EVENT_DESCRIPTION], + start=start, + end=end, + ), + ) + except ApiException as err: + raise HomeAssistantError(str(err)) from err diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 4e251b4b006..a871722c2e9 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -306,9 +306,12 @@ def mock_insert_event( ) -> Callable[[...], None]: """Fixture for capturing event creation.""" - def _expect_result(calendar_id: str = CALENDAR_ID) -> None: + def _expect_result( + calendar_id: str = CALENDAR_ID, exc: ClientError | None = None + ) -> None: aioclient_mock.post( f"{API_BASE_URL}/calendars/{calendar_id}/events", + exc=exc, ) return diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index d9b9ec8ed03..a40b499f769 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -8,6 +8,7 @@ import time from typing import Any from unittest.mock import Mock, patch +from aiohttp.client_exceptions import ClientError import pytest import voluptuous as vol @@ -21,6 +22,7 @@ from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -676,6 +678,33 @@ async def test_add_event_date_time( } +async def test_add_event_failure( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + mock_insert_event: Callable[[..., dict[str, Any]], None], + setup_config_entry: MockConfigEntry, + add_event_call_service: Callable[dict[str, Any], Awaitable[None]], +) -> None: + """Test service calls with incorrect fields.""" + + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + assert await component_setup() + + mock_insert_event( + calendar_id=CALENDAR_ID, + exc=ClientError(), + ) + + with pytest.raises(HomeAssistantError): + await add_event_call_service( + {"start_date": "2022-05-01", "end_date": "2022-05-01"} + ) + + @pytest.mark.parametrize( "config_entry_token_expiry", [datetime.datetime.max.timestamp() + 1] ) From 97426911a3bac20128f3558a3644b565b7024943 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Jul 2022 05:50:31 +0200 Subject: [PATCH 213/820] Update lxml to 4.9.1 (#74663) --- homeassistant/components/scrape/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index b1ccbb354a9..4f8ea3d1481 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -2,7 +2,7 @@ "domain": "scrape", "name": "Scrape", "documentation": "https://www.home-assistant.io/integrations/scrape", - "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], + "requirements": ["beautifulsoup4==4.11.1", "lxml==4.9.1"], "after_dependencies": ["rest"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 9f9423496fd..6cc0cce2cbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -984,7 +984,7 @@ lupupy==0.0.24 lw12==0.9.2 # homeassistant.components.scrape -lxml==4.8.0 +lxml==4.9.1 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 870f830ac31..32a642581d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -685,7 +685,7 @@ logi_circle==0.2.3 luftdaten==0.7.2 # homeassistant.components.scrape -lxml==4.8.0 +lxml==4.9.1 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 From ba0b98ef3219fd5d47b83cfc3611aa02663e730b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Jul 2022 08:30:01 +0200 Subject: [PATCH 214/820] Remove deprecated Spotify YAML configuration (#74604) --- homeassistant/components/spotify/__init__.py | 57 +----------- tests/components/spotify/test_config_flow.py | 91 ++++++++++---------- 2 files changed, 47 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index bf4e9d8deae..560620beed4 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -3,25 +3,14 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -import logging from typing import Any import aiohttp import requests from spotipy import Spotify, SpotifyException -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_CREDENTIALS, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - Platform, -) +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -29,7 +18,6 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .browse_media import async_browse_media @@ -40,23 +28,7 @@ from .util import ( spotify_uri_from_media_browser_url, ) -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Inclusive(CONF_CLIENT_ID, ATTR_CREDENTIALS): cv.string, - vol.Inclusive(CONF_CLIENT_SECRET, ATTR_CREDENTIALS): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER] @@ -79,31 +51,6 @@ class HomeAssistantSpotifyData: session: OAuth2Session -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Spotify integration.""" - if DOMAIN not in config: - return True - - if CONF_CLIENT_ID in config[DOMAIN]: - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of Spotify integration in YAML is deprecated and " - "will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Spotify from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index a47d25aa06d..8f3b5799374 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -2,14 +2,20 @@ from http import HTTPStatus from unittest.mock import patch +import pytest from spotipy import SpotifyException -from homeassistant import data_entry_flow, setup +from homeassistant import data_entry_flow from homeassistant.components import zeroconf +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.spotify.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -24,6 +30,19 @@ BLANK_ZEROCONF_INFO = zeroconf.ZeroconfServiceInfo( ) +@pytest.fixture +async def component_setup(hass: HomeAssistant) -> None: + """Fixture for setting up the integration.""" + result = await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + await async_import_client_credential( + hass, DOMAIN, ClientCredential("client", "secret"), "cred" + ) + + assert result + + async def test_abort_if_no_configuration(hass): """Check flow aborts when no configuration is present.""" result = await hass.config_entries.flow.async_init( @@ -54,18 +73,13 @@ async def test_zeroconf_abort_if_existing_entry(hass): async def test_full_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Check a full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -114,7 +128,7 @@ async def test_full_flow( } result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == DOMAIN + assert result["data"]["auth_implementation"] == "cred" result["data"]["token"].pop("expires_at") assert result["data"]["name"] == "frenck" assert result["data"]["token"] == { @@ -126,18 +140,13 @@ async def test_full_flow( async def test_abort_if_spotify_error( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Check Spotify errors causes flow to abort.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -174,23 +183,18 @@ async def test_abort_if_spotify_error( async def test_reauthentication( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Test Spotify reauthentication.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - old_entry = MockConfigEntry( domain=DOMAIN, unique_id=123, version=1, - data={"id": "frenck", "auth_implementation": DOMAIN}, + data={"id": "frenck", "auth_implementation": "cred"}, ) old_entry.add_to_hass(hass) @@ -236,7 +240,7 @@ async def test_reauthentication( spotify_mock.return_value.current_user.return_value = {"id": "frenck"} result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == DOMAIN + assert result["data"]["auth_implementation"] == "cred" result["data"]["token"].pop("expires_at") assert result["data"]["token"] == { "refresh_token": "mock-refresh-token", @@ -247,23 +251,18 @@ async def test_reauthentication( async def test_reauth_account_mismatch( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Test Spotify reauthentication with different account.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - old_entry = MockConfigEntry( domain=DOMAIN, unique_id=123, version=1, - data={"id": "frenck", "auth_implementation": DOMAIN}, + data={"id": "frenck", "auth_implementation": "cred"}, ) old_entry.add_to_hass(hass) From fd7330ea7700ac18ee1d073d0b55299ccb045d53 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 8 Jul 2022 09:48:46 +0200 Subject: [PATCH 215/820] Bump NextDNS backend library (#74611) --- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/test_diagnostics.py | 10 ++++++---- tests/components/nextdns/test_sensor.py | 8 ++++---- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index fd3dd46f846..a427f930db8 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -3,7 +3,7 @@ "name": "NextDNS", "documentation": "https://www.home-assistant.io/integrations/nextdns", "codeowners": ["@bieniu"], - "requirements": ["nextdns==1.0.0"], + "requirements": ["nextdns==1.0.1"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["nextdns"] diff --git a/requirements_all.txt b/requirements_all.txt index 6cc0cce2cbf..3019833e03d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1092,7 +1092,7 @@ nextcloudmonitor==1.1.0 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.0 +nextdns==1.0.1 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32a642581d8..4e0136bcc15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -760,7 +760,7 @@ nexia==2.0.1 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.0 +nextdns==1.0.1 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 73844b6a2e3..ba4a4d2ccb4 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -43,11 +43,13 @@ async def test_entry_diagnostics(hass, hass_client): "doh_queries": 20, "doq_queries": 10, "dot_queries": 30, + "tcp_queries": 0, "udp_queries": 40, - "doh_queries_ratio": 22.2, - "doq_queries_ratio": 11.1, - "dot_queries_ratio": 33.3, - "udp_queries_ratio": 44.4, + "doh_queries_ratio": 20.0, + "doq_queries_ratio": 10.0, + "dot_queries_ratio": 30.0, + "tcp_queries_ratio": 0.0, + "udp_queries_ratio": 40.0, } assert result["status_coordinator_data"] == { "all_queries": 100, diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index fef2abafc85..8a7d13866f3 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -197,7 +197,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_https_queries_ratio") assert state - assert state.state == "22.2" + assert state.state == "20.0" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -217,7 +217,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_quic_queries_ratio") assert state - assert state.state == "11.1" + assert state.state == "10.0" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -237,7 +237,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_tls_queries_ratio") assert state - assert state.state == "33.3" + assert state.state == "30.0" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -347,7 +347,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_udp_queries_ratio") assert state - assert state.state == "44.4" + assert state.state == "40.0" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE From 06530ebfa1b1ee0f52caa6a3152e1f470cbc7b63 Mon Sep 17 00:00:00 2001 From: siyuan-nz <91467287+siyuan-nz@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:51:10 +1200 Subject: [PATCH 216/820] Add ssh-rsa as acceptable an host key algorithm (#74684) --- homeassistant/components/unifi_direct/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index f71498faa11..7a81975c0ba 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -80,7 +80,7 @@ class UnifiDeviceScanner(DeviceScanner): def _connect(self): """Connect to the Unifi AP SSH server.""" - self.ssh = pxssh.pxssh() + self.ssh = pxssh.pxssh(options={"HostKeyAlgorithms": "ssh-rsa"}) try: self.ssh.login( self.host, self.username, password=self.password, port=self.port From 3f8cfa3b0af3933ab5eeb2d4d92d81e5c59cadcf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Jul 2022 12:39:29 +0200 Subject: [PATCH 217/820] Always run pip_check in CI (#74706) --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2f97a8e4208..5320b9fe474 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -677,7 +677,6 @@ jobs: pip-check: runs-on: ubuntu-20.04 - if: needs.info.outputs.requirements == 'true' || github.event.inputs.full == 'true' needs: - info - base From 3b3766fbe0b7ab2ae2816a970414cfa431b4f903 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 8 Jul 2022 12:54:09 +0200 Subject: [PATCH 218/820] Bump deconz dependency to fix #74523 (#74710) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_sensor.py | 31 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index c19d75ec054..06f8b6c0376 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==96"], + "requirements": ["pydeconz==97"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3019833e03d..ea7c9fe5900 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1444,7 +1444,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==96 +pydeconz==97 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e0136bcc15..16fd3833b4d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -977,7 +977,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==96 +pydeconz==97 # homeassistant.components.dexcom pydexcom==0.2.3 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 658e11da906..5f11a4d7b0b 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -816,6 +816,37 @@ async def test_dont_add_sensor_if_state_is_none( assert len(hass.states.async_all()) == 0 +async def test_air_quality_sensor_without_ppb(hass, aioclient_mock): + """Test sensor with scaled data is not created if state is None.""" + data = { + "sensors": { + "1": { + "config": { + "on": True, + "reachable": True, + }, + "ep": 2, + "etag": "c2d2e42396f7c78e11e46c66e2ec0200", + "lastseen": "2020-11-20T22:48Z", + "manufacturername": "BOSCH", + "modelid": "AIR", + "name": "BOSCH Air quality sensor", + "state": { + "airquality": "poor", + "lastupdated": "2020-11-20T22:48:00.209", + }, + "swversion": "20200402", + "type": "ZHAAirQuality", + "uniqueid": "00:00:00:00:00:00:00:00-02-fdef", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 1 + + async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" data = { From 540ffe116ec1b0a2e11463854819338869001900 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Jul 2022 13:59:20 +0200 Subject: [PATCH 219/820] Update debugpy to 1.6.2 (#74692) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 3f31c5de758..3cc6c16e831 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.6.1"], + "requirements": ["debugpy==1.6.2"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index ea7c9fe5900..bdb840ea88a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.1 +debugpy==1.6.2 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16fd3833b4d..d37ff39e79d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -388,7 +388,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.1 +debugpy==1.6.2 # homeassistant.components.ihc # homeassistant.components.namecheapdns From e55a4dcab1f00da4d50e0f76364cbbd453f0c440 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Fri, 8 Jul 2022 15:16:13 +0200 Subject: [PATCH 220/820] Add missing strings for here_travel_time (#74641) * Add missing strings for here_travel_time * script.translations develop * Correct origin_menu option --- homeassistant/components/here_travel_time/strings.json | 7 +++++++ .../components/here_travel_time/translations/en.json | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/here_travel_time/strings.json b/homeassistant/components/here_travel_time/strings.json index e4a20a38d6b..dab135efc82 100644 --- a/homeassistant/components/here_travel_time/strings.json +++ b/homeassistant/components/here_travel_time/strings.json @@ -8,6 +8,13 @@ "mode": "Travel Mode" } }, + "origin_menu": { + "title": "Choose Origin", + "menu_options": { + "origin_coordinates": "Using a map location", + "origin_entity": "Using an entity" + } + }, "origin_coordinates": { "title": "Choose Origin", "data": { diff --git a/homeassistant/components/here_travel_time/translations/en.json b/homeassistant/components/here_travel_time/translations/en.json index d4f9984d945..f31d5a3783d 100644 --- a/homeassistant/components/here_travel_time/translations/en.json +++ b/homeassistant/components/here_travel_time/translations/en.json @@ -39,6 +39,13 @@ }, "title": "Choose Origin" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Using a map location", + "origin_entity": "Using an entity" + }, + "title": "Choose Origin" + }, "user": { "data": { "api_key": "API Key", From b2a30716586c4b662733287c55cc5c40ef9736f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 8 Jul 2022 16:19:32 +0200 Subject: [PATCH 221/820] Cleanup generic in NextDNS (#74705) Cleanup generic in nextdns --- homeassistant/components/nextdns/sensor.py | 64 +++++++--------------- 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 71d71ff287b..54d10ba66b9 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -43,7 +43,6 @@ PARALLEL_UPDATES = 1 class NextDnsSensorRequiredKeysMixin(Generic[TCoordinatorData]): """Class for NextDNS entity required keys.""" - coordinator_class: type[TCoordinatorData] coordinator_type: str value: Callable[[TCoordinatorData], StateType] @@ -57,9 +56,8 @@ class NextDnsSensorEntityDescription( SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsStatus]( key="all_queries", - coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -68,9 +66,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.all_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsStatus]( key="blocked_queries", - coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -79,9 +76,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.blocked_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsStatus]( key="relayed_queries", - coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -90,9 +86,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.relayed_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsStatus]( key="blocked_queries_ratio", - coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -101,9 +96,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.blocked_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="doh_queries", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -113,9 +107,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="dot_queries", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -125,9 +118,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.dot_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="doq_queries", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -137,9 +129,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="udp_queries", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -149,9 +140,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.udp_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="doh_queries_ratio", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_registry_enabled_default=False, icon="mdi:dns", @@ -161,9 +151,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="dot_queries_ratio", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -173,9 +162,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.dot_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="doq_queries_ratio", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_registry_enabled_default=False, icon="mdi:dns", @@ -185,9 +173,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="udp_queries_ratio", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -197,9 +184,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.udp_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsEncryption]( key="encrypted_queries", - coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -209,9 +195,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.encrypted_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsEncryption]( key="unencrypted_queries", - coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -221,9 +206,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.unencrypted_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsEncryption]( key="encrypted_queries_ratio", - coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -233,9 +217,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.encrypted_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsIpVersions]( key="ipv4_queries", - coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -245,9 +228,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv4_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsIpVersions]( key="ipv6_queries", - coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -257,9 +239,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv6_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsIpVersions]( key="ipv6_queries_ratio", - coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -269,9 +250,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv6_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsDnssec]( key="validated_queries", - coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -281,9 +261,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.validated_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsDnssec]( key="not_validated_queries", - coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -293,9 +272,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.not_validated_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsDnssec]( key="validated_queries_ratio", - coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, From 08d8304a529074a0b39b6ca83355ba799bf21afc Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 8 Jul 2022 07:59:50 -0700 Subject: [PATCH 222/820] Migrate google calendar to new entity naming (#74727) * Migrate google calendar to new entity naming * Update homeassistant/components/google/calendar.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/google/calendar.py | 9 +++------ tests/components/google/conftest.py | 4 ++-- tests/components/google/test_init.py | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index a86cdf55e3a..534b1cbdef3 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -216,6 +216,8 @@ async def async_setup_entry( class GoogleCalendarEntity(CalendarEntity): """A calendar event device.""" + _attr_has_entity_name = True + def __init__( self, calendar_service: GoogleCalendarService, @@ -231,7 +233,7 @@ class GoogleCalendarEntity(CalendarEntity): self._search: str | None = data.get(CONF_SEARCH) self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) self._event: CalendarEvent | None = None - self._name: str = data[CONF_NAME] + self._attr_name = data[CONF_NAME].capitalize() self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) self._offset_value: timedelta | None = None self.entity_id = entity_id @@ -257,11 +259,6 @@ class GoogleCalendarEntity(CalendarEntity): """Return the next upcoming event.""" return self._event - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - def _event_filter(self, event: Event) -> bool: """Return True if the event is visible.""" if self._ignore_availability: diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index a871722c2e9..f47d1232582 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -34,10 +34,10 @@ EMAIL_ADDRESS = "user@gmail.com" # the yaml config that overrides the entity name and other settings. A test # can use a fixture to exercise either case. TEST_API_ENTITY = "calendar.we_are_we_are_a_test_calendar" -TEST_API_ENTITY_NAME = "We are, we are, a... Test Calendar" +TEST_API_ENTITY_NAME = "We are, we are, a... test calendar" # Name of the entity when using yaml configuration overrides TEST_YAML_ENTITY = "calendar.backyard_light" -TEST_YAML_ENTITY_NAME = "Backyard Light" +TEST_YAML_ENTITY_NAME = "Backyard light" # A calendar object returned from the API TEST_API_CALENDAR = { diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index a40b499f769..f6c1f8c611f 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -20,7 +20,7 @@ from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT from homeassistant.components.google.calendar import SERVICE_CREATE_EVENT from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import STATE_OFF +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -307,8 +307,8 @@ async def test_multiple_config_entries( state = hass.states.get("calendar.example_calendar_1") assert state - assert state.name == "Example Calendar 1" assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Example calendar 1" config_entry2 = MockConfigEntry( domain=DOMAIN, data=config_entry.data, unique_id="other-address@example.com" @@ -327,7 +327,7 @@ async def test_multiple_config_entries( state = hass.states.get("calendar.example_calendar_2") assert state - assert state.name == "Example Calendar 2" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Example calendar 2" @pytest.mark.parametrize( From 1d69e631b53b60f03add725aac664b70e060a6c5 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 8 Jul 2022 18:47:59 +0200 Subject: [PATCH 223/820] Fix ZHA group not setting the correct color mode (#74687) * Fix ZHA group not setting the correct color mode * Changed to use _attr_color_mode --- homeassistant/components/zha/light.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 309fdf2699b..e1c85b39d8e 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -141,7 +141,7 @@ class BaseLight(LogMixin, light.LightEntity): self._color_channel = None self._identify_channel = None self._default_transition = None - self._color_mode = ColorMode.UNKNOWN # Set by sub classes + self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes @property def extra_state_attributes(self) -> dict[str, Any]: @@ -159,11 +159,6 @@ class BaseLight(LogMixin, light.LightEntity): return False return self._state - @property - def color_mode(self): - """Return the color mode of this light.""" - return self._color_mode - @property def brightness(self): """Return the brightness of this light.""" @@ -309,7 +304,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP self._color_temp = temperature self._hs_color = None @@ -323,7 +318,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS self._hs_color = hs_color self._color_temp = None @@ -451,13 +446,13 @@ class Light(BaseLight, ZhaEntity): self._attr_supported_color_modes ) if len(self._attr_supported_color_modes) == 1: - self._color_mode = next(iter(self._attr_supported_color_modes)) + self._attr_color_mode = next(iter(self._attr_supported_color_modes)) else: # Light supports color_temp + hs, determine which mode the light is in assert self._color_channel if self._color_channel.color_mode == Color.ColorMode.Color_temperature: - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP else: - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS if self._identify_channel: self._supported_features |= light.LightEntityFeature.FLASH @@ -518,7 +513,7 @@ class Light(BaseLight, ZhaEntity): if "off_brightness" in last_state.attributes: self._off_brightness = last_state.attributes["off_brightness"] if "color_mode" in last_state.attributes: - self._color_mode = ColorMode(last_state.attributes["color_mode"]) + self._attr_color_mode = ColorMode(last_state.attributes["color_mode"]) if "color_temp" in last_state.attributes: self._color_temp = last_state.attributes["color_temp"] if "hs_color" in last_state.attributes: @@ -558,13 +553,13 @@ class Light(BaseLight, ZhaEntity): if (color_mode := results.get("color_mode")) is not None: if color_mode == Color.ColorMode.Color_temperature: - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP color_temp = results.get("color_temperature") if color_temp is not None and color_mode: self._color_temp = color_temp self._hs_color = None else: - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS color_x = results.get("current_x") color_y = results.get("current_y") if color_x is not None and color_y is not None: @@ -650,7 +645,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) - self._color_mode = None + self._attr_color_mode = None async def async_added_to_hass(self): """Run when about to be added to hass.""" From ff324ab2fb0a95f49f98bbda0c9fcb6646da4d7b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 8 Jul 2022 09:50:56 -0700 Subject: [PATCH 224/820] Attempt to fix flaky test by waiting for setup to complete (#74734) --- tests/components/system_log/test_init.py | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 121c29d2eed..d558d690536 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -89,6 +89,7 @@ def get_frame(name): async def test_normal_logs(hass, simple_queue, hass_ws_client): """Test that debug and info are not logged.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() _LOGGER.debug("debug") _LOGGER.info("info") @@ -102,6 +103,8 @@ async def test_normal_logs(hass, simple_queue, hass_ws_client): async def test_exception(hass, simple_queue, hass_ws_client): """Test that exceptions are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _generate_and_log_exception("exception message", "log message") await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "ERROR") @@ -112,6 +115,8 @@ async def test_exception(hass, simple_queue, hass_ws_client): async def test_warning(hass, simple_queue, hass_ws_client): """Test that warning are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.warning("warning message") await _async_block_until_queue_empty(hass, simple_queue) @@ -122,6 +127,8 @@ async def test_warning(hass, simple_queue, hass_ws_client): async def test_error(hass, simple_queue, hass_ws_client): """Test that errors are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.error("error message") await _async_block_until_queue_empty(hass, simple_queue) @@ -132,6 +139,8 @@ async def test_error(hass, simple_queue, hass_ws_client): async def test_config_not_fire_event(hass, simple_queue): """Test that errors are not posted as events with default config.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + events = [] @callback @@ -152,6 +161,8 @@ async def test_error_posted_as_event(hass, simple_queue): await async_setup_component( hass, system_log.DOMAIN, {"system_log": {"max_entries": 2, "fire_event": True}} ) + await hass.async_block_till_done() + events = async_capture_events(hass, system_log.EVENT_SYSTEM_LOG) _LOGGER.error("error message") @@ -164,6 +175,8 @@ async def test_error_posted_as_event(hass, simple_queue): async def test_critical(hass, simple_queue, hass_ws_client): """Test that critical are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.critical("critical message") await _async_block_until_queue_empty(hass, simple_queue) @@ -174,6 +187,8 @@ async def test_critical(hass, simple_queue, hass_ws_client): async def test_remove_older_logs(hass, simple_queue, hass_ws_client): """Test that older logs are rotated out.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.error("error message 1") _LOGGER.error("error message 2") _LOGGER.error("error message 3") @@ -192,6 +207,8 @@ def log_msg(nr=2): async def test_dedupe_logs(hass, simple_queue, hass_ws_client): """Test that duplicate log entries are dedupe.""" await async_setup_component(hass, system_log.DOMAIN, {}) + await hass.async_block_till_done() + _LOGGER.error("error message 1") log_msg() log_msg("2-2") @@ -234,6 +251,8 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): async def test_clear_logs(hass, simple_queue, hass_ws_client): """Test that the log can be cleared via a service call.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.error("error message") await _async_block_until_queue_empty(hass, simple_queue) @@ -247,6 +266,8 @@ async def test_clear_logs(hass, simple_queue, hass_ws_client): async def test_write_log(hass): """Test that error propagates to logger.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + logger = MagicMock() with patch("logging.getLogger", return_value=logger) as mock_logging: await hass.services.async_call( @@ -260,6 +281,8 @@ async def test_write_log(hass): async def test_write_choose_logger(hass): """Test that correct logger is chosen.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + with patch("logging.getLogger") as mock_logging: await hass.services.async_call( system_log.DOMAIN, @@ -273,6 +296,8 @@ async def test_write_choose_logger(hass): async def test_write_choose_level(hass): """Test that correct logger is chosen.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + logger = MagicMock() with patch("logging.getLogger", return_value=logger): await hass.services.async_call( @@ -287,6 +312,8 @@ async def test_write_choose_level(hass): async def test_unknown_path(hass, simple_queue, hass_ws_client): """Test error logged from unknown path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.findCaller = MagicMock(return_value=("unknown_path", 0, None, None)) _LOGGER.error("error message") await _async_block_until_queue_empty(hass, simple_queue) @@ -317,6 +344,8 @@ async def async_log_error_from_test_path(hass, path, sq): async def test_homeassistant_path(hass, simple_queue, hass_ws_client): """Test error logged from Home Assistant path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + with patch( "homeassistant.components.system_log.HOMEASSISTANT_PATH", new=["venv_path/homeassistant"], @@ -331,6 +360,8 @@ async def test_homeassistant_path(hass, simple_queue, hass_ws_client): async def test_config_path(hass, simple_queue, hass_ws_client): """Test error logged from config path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + with patch.object(hass.config, "config_dir", new="config"): await async_log_error_from_test_path( hass, "config/custom_component/test.py", simple_queue From 4209d7733b3983cc45f61b4eb6c758f5edb314b0 Mon Sep 17 00:00:00 2001 From: Antonino Piazza Date: Fri, 8 Jul 2022 22:09:03 +0200 Subject: [PATCH 225/820] Add huawei_lte wifi guest network switch (#71035) --- .../components/huawei_lte/__init__.py | 13 ++ homeassistant/components/huawei_lte/const.py | 3 +- homeassistant/components/huawei_lte/switch.py | 47 +++++- tests/components/huawei_lte/test_switches.py | 138 ++++++++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 tests/components/huawei_lte/test_switches.py diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index f4e2cb209db..c0337095a9c 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -74,6 +74,7 @@ from .const import ( KEY_SMS_SMS_COUNT, KEY_WLAN_HOST_LIST, KEY_WLAN_WIFI_FEATURE_SWITCH, + KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, NOTIFY_SUPPRESS_TIMEOUT, SERVICE_CLEAR_TRAFFIC_STATISTICS, SERVICE_REBOOT, @@ -275,6 +276,18 @@ class Router: self._get_data( KEY_WLAN_WIFI_FEATURE_SWITCH, self.client.wlan.wifi_feature_switch ) + self._get_data( + KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, + lambda: next( + filter( + lambda ssid: ssid.get("wifiisguestnetwork") == "1", + self.client.wlan.multi_basic_settings() + .get("Ssids", {}) + .get("Ssid", []), + ), + {}, + ), + ) dispatcher_send(self.hass, UPDATE_SIGNAL, self.config_entry.unique_id) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index b9cbf546087..754be6bf2f3 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -43,6 +43,7 @@ KEY_NET_NET_MODE = "net_net_mode" KEY_SMS_SMS_COUNT = "sms_sms_count" KEY_WLAN_HOST_LIST = "wlan_host_list" KEY_WLAN_WIFI_FEATURE_SWITCH = "wlan_wifi_feature_switch" +KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH = "wlan_wifi_guest_network_switch" BINARY_SENSOR_KEYS = { KEY_MONITORING_CHECK_NOTIFICATIONS, @@ -67,7 +68,7 @@ SENSOR_KEYS = { KEY_SMS_SMS_COUNT, } -SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH} +SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH, KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH} ALL_KEYS = ( BINARY_SENSOR_KEYS diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index cc5e8e446c5..611f7212d9a 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -16,7 +16,11 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HuaweiLteBaseEntityWithDevice -from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH +from .const import ( + DOMAIN, + KEY_DIALUP_MOBILE_DATASWITCH, + KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, +) _LOGGER = logging.getLogger(__name__) @@ -33,6 +37,9 @@ async def async_setup_entry( if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): switches.append(HuaweiLteMobileDataSwitch(router)) + if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH).get("WifiEnable"): + switches.append(HuaweiLteWifiGuestNetworkSwitch(router)) + async_add_entities(switches, True) @@ -113,3 +120,41 @@ class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): def icon(self) -> str: """Return switch icon.""" return "mdi:signal" if self.is_on else "mdi:signal-off" + + +@dataclass +class HuaweiLteWifiGuestNetworkSwitch(HuaweiLteBaseSwitch): + """Huawei LTE WiFi guest network switch device.""" + + def __post_init__(self) -> None: + """Initialize identifiers.""" + self.key = KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH + self.item = "WifiEnable" + + @property + def _entity_name(self) -> str: + return "WiFi guest network" + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + + @property + def is_on(self) -> bool: + """Return whether the switch is on.""" + return self._raw_state == "1" + + def _turn(self, state: bool) -> None: + self.router.client.wlan.wifi_guest_network_switch(state) + self._raw_state = "1" if state else "0" + self.schedule_update_ha_state() + + @property + def icon(self) -> str: + """Return switch icon.""" + return "mdi:wifi" if self.is_on else "mdi:wifi-off" + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the state attributes.""" + return {"ssid": self.router.data[self.key].get("WifiSsid")} diff --git a/tests/components/huawei_lte/test_switches.py b/tests/components/huawei_lte/test_switches.py new file mode 100644 index 00000000000..c5006add923 --- /dev/null +++ b/tests/components/huawei_lte/test_switches.py @@ -0,0 +1,138 @@ +"""Tests for the Huawei LTE switches.""" +from unittest.mock import MagicMock, patch + +from huawei_lte_api.enums.cradle import ConnectionStatusEnum +from pytest import fixture + +from homeassistant.components.huawei_lte.const import DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_URL, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry + +from tests.common import MockConfigEntry + +SWITCH_WIFI_GUEST_NETWORK = "switch.huawei_lte_wifi_guest_network" + + +@fixture +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=MagicMock( + device=MagicMock( + information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) + ), + monitoring=MagicMock( + check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), + status=MagicMock( + return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} + ), + ), + wlan=MagicMock( + multi_basic_settings=MagicMock( + return_value={ + "Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]} + } + ), + wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), + ), + ), +) +async def setup_component_with_wifi_guest_network( + client: MagicMock, hass: HomeAssistant +) -> None: + """Initialize huawei_lte components.""" + assert client + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + assert await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + + +@fixture +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=MagicMock( + device=MagicMock( + information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) + ), + monitoring=MagicMock( + check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), + status=MagicMock( + return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} + ), + ), + wlan=MagicMock( + multi_basic_settings=MagicMock(return_value={}), + wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), + ), + ), +) +async def setup_component_without_wifi_guest_network( + client: MagicMock, hass: HomeAssistant +) -> None: + """Initialize huawei_lte components.""" + assert client + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + assert await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + + +def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_not_present( + hass: HomeAssistant, + setup_component_without_wifi_guest_network, +) -> None: + """Test switch wifi guest network config entry when network is not present.""" + entity_registry: EntityRegistry = er.async_get(hass) + assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) + + +def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_present( + hass: HomeAssistant, + setup_component_with_wifi_guest_network, +) -> None: + """Test switch wifi guest network config entry when network is present.""" + entity_registry: EntityRegistry = er.async_get(hass) + assert entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) + + +async def test_turn_on_switch_wifi_guest_network( + hass: HomeAssistant, setup_component_with_wifi_guest_network +) -> None: + """Test switch wifi guest network turn on method.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: SWITCH_WIFI_GUEST_NETWORK}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_ON) + hass.data[DOMAIN].routers[ + "test-serial-number" + ].client.wlan.wifi_guest_network_switch.assert_called_once_with(True) + + +async def test_turn_off_switch_wifi_guest_network( + hass: HomeAssistant, setup_component_with_wifi_guest_network +) -> None: + """Test switch wifi guest network turn off method.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: SWITCH_WIFI_GUEST_NETWORK}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_OFF) + hass.data[DOMAIN].routers[ + "test-serial-number" + ].client.wlan.wifi_guest_network_switch.assert_called_with(False) From 010b18be34849baec7d956e96ae4bcb386d87025 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jul 2022 14:17:56 -0700 Subject: [PATCH 226/820] Bump atomicwrites (#74758) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index d5d0c2c0370..4987169d280 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.54.0"], + "requirements": ["hass-nabucasa==0.54.1"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a388f178df3..8f0e2087c14 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.31.2 async_timeout==4.0.2 -atomicwrites==1.4.0 +atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 -hass-nabucasa==0.54.0 +hass-nabucasa==0.54.1 home-assistant-frontend==20220707.0 httpx==0.23.0 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index 60e0865fab9..621bbf68999 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "astral==2.2", "async_timeout==4.0.2", "attrs==21.2.0", - "atomicwrites==1.4.0", + "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.6.0", "bcrypt==3.1.7", "certifi>=2021.5.30", diff --git a/requirements.txt b/requirements.txt index 7826a9a0f26..fefa0f33ecb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ aiohttp==3.8.1 astral==2.2 async_timeout==4.0.2 attrs==21.2.0 -atomicwrites==1.4.0 +atomicwrites-homeassistant==1.4.1 awesomeversion==22.6.0 bcrypt==3.1.7 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index bdb840ea88a..7c5512c6c00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -795,7 +795,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.0 +hass-nabucasa==0.54.1 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d37ff39e79d..6c16b073632 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -577,7 +577,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.0 +hass-nabucasa==0.54.1 # homeassistant.components.tasmota hatasmota==0.5.1 From 0cca086aab84d63db4364622bfd4a79bc75479a5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 8 Jul 2022 15:18:40 -0600 Subject: [PATCH 227/820] Bump regenmaschine to 2022.07.0 (#74680) --- homeassistant/components/rainmachine/__init__.py | 2 +- homeassistant/components/rainmachine/config_flow.py | 2 +- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 6d51be9d921..c285bf89e57 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -183,7 +183,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], port=entry.data[CONF_PORT], - ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), + use_ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), ) except RainMachineError as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index d24dae46c2b..c12362591e7 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -32,7 +32,7 @@ async def async_get_controller( websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) try: - await client.load_local(ip_address, password, port=port, ssl=ssl) + await client.load_local(ip_address, password, port=port, use_ssl=ssl) except RainMachineError: return None else: diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index e9df60e4697..4f06ed0d71b 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.06.1"], + "requirements": ["regenmaschine==2022.07.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 7c5512c6c00..d4e792ee233 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.06.1 +regenmaschine==2022.07.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c16b073632..4bc006c0b54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1379,7 +1379,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.06.1 +regenmaschine==2022.07.0 # homeassistant.components.renault renault-api==0.1.11 From 766523cf8cda9f3cae68a25d48388a62aa4919f0 Mon Sep 17 00:00:00 2001 From: Benoit Anastay <45088785+BenoitAnastay@users.noreply.github.com> Date: Fri, 8 Jul 2022 23:22:31 +0200 Subject: [PATCH 228/820] Fix error with HDD temperature report in Freebox integration (#74718) * Fix error whith HDD temperature report There was a non handled error case, documented in issue https://github.com/home-assistant/core/issues/43812 back in 2020 and the fix wasn't applied * Use get method instead of ignoring the sensor * Update test values Add idle state drive with unkown temp * update Tests for system sensors api * Fix booleans values * Fix disk unique_id There was a typo in the code --- homeassistant/components/freebox/router.py | 2 +- homeassistant/components/freebox/sensor.py | 2 +- tests/components/freebox/const.py | 38 +++++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 70fc7b86a40..ce4d03aae7a 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -113,7 +113,7 @@ class FreeboxRouter: # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. # Name and id of sensors may vary under Freebox devices. for sensor in syst_datas["sensors"]: - self.sensors_temperature[sensor["name"]] = sensor["value"] + self.sensors_temperature[sensor["name"]] = sensor.get("value") # Connection sensors connection_datas: dict[str, Any] = await self._api.connection.get_status() diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 46aa9ee8aa0..450456b9146 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -159,7 +159,7 @@ class FreeboxDiskSensor(FreeboxSensor): self._disk = disk self._partition = partition self._attr_name = f"{partition['label']} {description.name}" - self._unique_id = f"{self._router.mac} {description.key} {self._disk['id']} {self._partition['id']}" + self._attr_unique_id = f"{self._router.mac} {description.key} {self._disk['id']} {self._partition['id']}" @property def device_info(self) -> DeviceInfo: diff --git a/tests/components/freebox/const.py b/tests/components/freebox/const.py index cc3d720d7ef..25402cbcdef 100644 --- a/tests/components/freebox/const.py +++ b/tests/components/freebox/const.py @@ -22,6 +22,7 @@ DATA_SYSTEM_GET_CONFIG = { "fans": [{"id": "fan0_speed", "name": "Ventilateur 1", "value": 2130}], "sensors": [ {"id": "temp_hdd", "name": "Disque dur", "value": 40}, + {"id": "temp_hdd2", "name": "Disque dur 2"}, {"id": "temp_sw", "name": "Température Switch", "value": 50}, {"id": "temp_cpum", "name": "Température CPU M", "value": 60}, {"id": "temp_cpub", "name": "Température CPU B", "value": 56}, @@ -123,7 +124,42 @@ DATA_STORAGE_GET_DISKS = [ "path": "L0Rpc3F1ZSBkdXI=", } ], - } + }, + { + "idle_duration": 8290, + "read_error_requests": 0, + "read_requests": 2326826, + "spinning": False, + "table_type": "gpt", + "firmware": "0001", + "type": "sata", + "idle": True, + "connector": 0, + "id": 2000, + "write_error_requests": 0, + "state": "enabled", + "write_requests": 122733632, + "total_bytes": 2000000000000, + "model": "ST2000LM015-2E8174", + "active_duration": 0, + "temp": 0, + "serial": "WDZYJ27Q", + "partitions": [ + { + "fstype": "ext4", + "total_bytes": 1960000000000, + "label": "Disque 2", + "id": 2001, + "internal": False, + "fsck_result": "no_run_yet", + "state": "mounted", + "disk_id": 2000, + "free_bytes": 1880000000000, + "used_bytes": 85410000000, + "path": "L0Rpc3F1ZSAy", + } + ], + }, ] # switch From c27fbce7d09a8a067c8915cee56efd53a5aa2323 Mon Sep 17 00:00:00 2001 From: kpine Date: Fri, 8 Jul 2022 15:20:44 -0700 Subject: [PATCH 229/820] Fix KeyError from zwave_js diagnostics (#74579) --- .../components/zwave_js/diagnostics.py | 29 ++++--- tests/components/zwave_js/test_diagnostics.py | 80 ++++++++++++++++++- 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 3372b0eeec0..078bd761b71 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -94,20 +94,23 @@ def get_device_entities( # If the value ID returns as None, we don't need to include this entity if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None: continue - state_key = get_state_key_from_unique_id(entry.unique_id) - zwave_value = node.values[value_id] - primary_value_data = { - "command_class": zwave_value.command_class, - "command_class_name": zwave_value.command_class_name, - "endpoint": zwave_value.endpoint, - "property": zwave_value.property_, - "property_name": zwave_value.property_name, - "property_key": zwave_value.property_key, - "property_key_name": zwave_value.property_key_name, - } - if state_key is not None: - primary_value_data["state_key"] = state_key + primary_value_data = None + if (zwave_value := node.values.get(value_id)) is not None: + primary_value_data = { + "command_class": zwave_value.command_class, + "command_class_name": zwave_value.command_class_name, + "endpoint": zwave_value.endpoint, + "property": zwave_value.property_, + "property_name": zwave_value.property_name, + "property_key": zwave_value.property_key, + "property_key_name": zwave_value.property_key_name, + } + + state_key = get_state_key_from_unique_id(entry.unique_id) + if state_key is not None: + primary_value_data["state_key"] = state_key + entity = { "domain": entry.domain, "entity_id": entry.entity_id, diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 3ac3f32b45a..9f3a7b0884c 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -10,8 +10,12 @@ from homeassistant.components.zwave_js.diagnostics import ( async_get_device_diagnostics, ) from homeassistant.components.zwave_js.discovery import async_discover_node_values -from homeassistant.components.zwave_js.helpers import get_device_id -from homeassistant.helpers.device_registry import async_get +from homeassistant.components.zwave_js.helpers import ( + get_device_id, + get_value_id_from_unique_id, +) +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from .common import PROPERTY_ULTRAVIOLET @@ -53,7 +57,7 @@ async def test_device_diagnostics( version_state, ): """Test the device level diagnostics data dump.""" - dev_reg = async_get(hass) + dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) assert device @@ -106,7 +110,7 @@ async def test_device_diagnostics( async def test_device_diagnostics_error(hass, integration): """Test the device diagnostics raises exception when an invalid device is used.""" - dev_reg = async_get(hass) + dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_or_create( config_entry_id=integration.entry_id, identifiers={("test", "test")} ) @@ -118,3 +122,71 @@ async def test_empty_zwave_value_matcher(): """Test empty ZwaveValueMatcher is invalid.""" with pytest.raises(ValueError): ZwaveValueMatcher() + + +async def test_device_diagnostics_missing_primary_value( + hass, + client, + multisensor_6, + integration, + hass_client, +): + """Test that the device diagnostics handles an entity with a missing primary value.""" + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) + assert device + + entity_id = "sensor.multisensor_6_air_temperature" + ent_reg = async_get_ent_reg(hass) + entry = ent_reg.async_get(entity_id) + + # check that the primary value for the entity exists in the diagnostics + diagnostics_data = await get_diagnostics_for_device( + hass, hass_client, integration, device + ) + + value = multisensor_6.values.get(get_value_id_from_unique_id(entry.unique_id)) + assert value + + air_entity = next( + x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id + ) + + assert air_entity["primary_value"] == { + "command_class": value.command_class, + "command_class_name": value.command_class_name, + "endpoint": value.endpoint, + "property": value.property_, + "property_name": value.property_name, + "property_key": value.property_key, + "property_key_name": value.property_key_name, + } + + # make the entity's primary value go missing + event = Event( + type="value removed", + data={ + "source": "node", + "event": "value removed", + "nodeId": multisensor_6.node_id, + "args": { + "commandClassName": value.command_class_name, + "commandClass": value.command_class, + "endpoint": value.endpoint, + "property": value.property_, + "prevValue": 0, + "propertyName": value.property_name, + }, + }, + ) + multisensor_6.receive_event(event) + + diagnostics_data = await get_diagnostics_for_device( + hass, hass_client, integration, device + ) + + air_entity = next( + x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id + ) + + assert air_entity["primary_value"] is None From a697672944b8bbdd32e3dad8eca93bd9ae8b40a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 8 Jul 2022 18:55:31 -0500 Subject: [PATCH 230/820] Add bluetooth integration (#74653) Co-authored-by: Paulus Schoutsen --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/bootstrap.py | 2 +- .../components/bluetooth/__init__.py | 297 ++++++++++++ homeassistant/components/bluetooth/const.py | 3 + .../components/bluetooth/manifest.json | 10 + homeassistant/components/bluetooth/models.py | 142 ++++++ homeassistant/components/bluetooth/usage.py | 13 + .../components/switchbot/manifest.json | 1 + homeassistant/config_entries.py | 9 + homeassistant/generated/bluetooth.py | 14 + homeassistant/helpers/config_entry_flow.py | 13 +- homeassistant/loader.py | 42 ++ mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/hassfest/__main__.py | 2 + script/hassfest/bluetooth.py | 65 +++ script/hassfest/config_flow.py | 1 + script/hassfest/manifest.py | 10 + tests/components/bluetooth/__init__.py | 1 + tests/components/bluetooth/conftest.py | 25 + tests/components/bluetooth/test_init.py | 440 ++++++++++++++++++ tests/components/bluetooth/test_usage.py | 22 + tests/helpers/test_config_entry_flow.py | 3 + tests/test_config_entries.py | 1 + tests/test_loader.py | 45 ++ 27 files changed, 1179 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bluetooth/__init__.py create mode 100644 homeassistant/components/bluetooth/const.py create mode 100644 homeassistant/components/bluetooth/manifest.json create mode 100644 homeassistant/components/bluetooth/models.py create mode 100644 homeassistant/components/bluetooth/usage.py create mode 100644 homeassistant/generated/bluetooth.py create mode 100644 script/hassfest/bluetooth.py create mode 100644 tests/components/bluetooth/__init__.py create mode 100644 tests/components/bluetooth/conftest.py create mode 100644 tests/components/bluetooth/test_init.py create mode 100644 tests/components/bluetooth/test_usage.py diff --git a/.strict-typing b/.strict-typing index c5ed000a7ce..becd5cdda9a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -57,6 +57,7 @@ homeassistant.components.automation.* homeassistant.components.backup.* homeassistant.components.baf.* homeassistant.components.binary_sensor.* +homeassistant.components.bluetooth.* homeassistant.components.bluetooth_tracker.* homeassistant.components.bmw_connected_drive.* homeassistant.components.bond.* diff --git a/CODEOWNERS b/CODEOWNERS index fda94805214..4e5d07f54d8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -138,6 +138,8 @@ build.json @home-assistant/supervisor /homeassistant/components/blueprint/ @home-assistant/core /tests/components/blueprint/ @home-assistant/core /homeassistant/components/bluesound/ @thrawnarn +/homeassistant/components/bluetooth/ @bdraco +/tests/components/bluetooth/ @bdraco /homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe /tests/components/bmw_connected_drive/ @gerard33 @rikroe /homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 939d3073f57..5d19249e37b 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -70,7 +70,7 @@ LOGGING_INTEGRATIONS = { # To record data "recorder", } -DISCOVERY_INTEGRATIONS = ("dhcp", "ssdp", "usb", "zeroconf") +DISCOVERY_INTEGRATIONS = ("bluetooth", "dhcp", "ssdp", "usb", "zeroconf") STAGE_1_INTEGRATIONS = { # We need to make sure discovery integrations # update their deps before stage 2 integrations diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py new file mode 100644 index 00000000000..cf7f1884869 --- /dev/null +++ b/homeassistant/components/bluetooth/__init__.py @@ -0,0 +1,297 @@ +"""The bluetooth integration.""" +from __future__ import annotations + +from collections.abc import Callable +import dataclasses +from enum import Enum +import fnmatch +from functools import cached_property +import logging +import platform +from typing import Final + +from bleak import BleakError +from bleak.backends.device import MANUFACTURERS, BLEDevice +from bleak.backends.scanner import AdvertisementData +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + callback as hass_callback, +) +from homeassistant.data_entry_flow import BaseServiceInfo +from homeassistant.helpers import discovery_flow +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import BluetoothMatcher, async_get_bluetooth + +from . import models +from .const import DOMAIN +from .models import HaBleakScanner +from .usage import install_multiple_bleak_catcher + +_LOGGER = logging.getLogger(__name__) + +MAX_REMEMBER_ADDRESSES: Final = 2048 + + +class BluetoothScanningMode(Enum): + """The mode of scanning for bluetooth devices.""" + + PASSIVE = "passive" + ACTIVE = "active" + + +SCANNING_MODE_TO_BLEAK = { + BluetoothScanningMode.ACTIVE: "active", + BluetoothScanningMode.PASSIVE: "passive", +} + +LOCAL_NAME: Final = "local_name" +SERVICE_UUID: Final = "service_uuid" +MANUFACTURER_ID: Final = "manufacturer_id" +MANUFACTURER_DATA_FIRST_BYTE: Final = "manufacturer_data_first_byte" + + +@dataclasses.dataclass +class BluetoothServiceInfo(BaseServiceInfo): + """Prepared info from bluetooth entries.""" + + name: str + address: str + rssi: int + manufacturer_data: dict[int, bytes] + service_data: dict[str, bytes] + service_uuids: list[str] + + @classmethod + def from_advertisement( + cls, device: BLEDevice, advertisement_data: AdvertisementData + ) -> BluetoothServiceInfo: + """Create a BluetoothServiceInfo from an advertisement.""" + return cls( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + ) + + @cached_property + def manufacturer(self) -> str | None: + """Convert manufacturer data to a string.""" + for manufacturer in self.manufacturer_data: + if manufacturer in MANUFACTURERS: + name: str = MANUFACTURERS[manufacturer] + return name + return None + + @cached_property + def manufacturer_id(self) -> int | None: + """Get the first manufacturer id.""" + for manufacturer in self.manufacturer_data: + return manufacturer + return None + + +BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") +BluetoothCallback = Callable[[BluetoothServiceInfo, BluetoothChange], None] + + +@hass_callback +def async_register_callback( + hass: HomeAssistant, + callback: BluetoothCallback, + match_dict: BluetoothMatcher | None, +) -> Callable[[], None]: + """Register to receive a callback on bluetooth change. + + Returns a callback that can be used to cancel the registration. + """ + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_register_callback(callback, match_dict) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the bluetooth integration.""" + integration_matchers = await async_get_bluetooth(hass) + bluetooth_discovery = BluetoothManager( + hass, integration_matchers, BluetoothScanningMode.PASSIVE + ) + await bluetooth_discovery.async_setup() + hass.data[DOMAIN] = bluetooth_discovery + return True + + +def _ble_device_matches( + matcher: BluetoothMatcher, device: BLEDevice, advertisement_data: AdvertisementData +) -> bool: + """Check if a ble device and advertisement_data matches the matcher.""" + if ( + matcher_local_name := matcher.get(LOCAL_NAME) + ) is not None and not fnmatch.fnmatch( + advertisement_data.local_name or device.name or device.address, + matcher_local_name, + ): + return False + + if ( + matcher_service_uuid := matcher.get(SERVICE_UUID) + ) is not None and matcher_service_uuid not in advertisement_data.service_uuids: + return False + + if ( + (matcher_manfacturer_id := matcher.get(MANUFACTURER_ID)) is not None + and matcher_manfacturer_id not in advertisement_data.manufacturer_data + ): + return False + + if ( + matcher_manufacturer_data_first_byte := matcher.get( + MANUFACTURER_DATA_FIRST_BYTE + ) + ) is not None and not any( + matcher_manufacturer_data_first_byte == manufacturer_data[0] + for manufacturer_data in advertisement_data.manufacturer_data.values() + ): + return False + + return True + + +@hass_callback +def async_enable_rssi_updates() -> None: + """Bleak filters out RSSI updates by default on linux only.""" + # We want RSSI updates + if platform.system() == "Linux": + from bleak.backends.bluezdbus import ( # pylint: disable=import-outside-toplevel + scanner, + ) + + scanner._ADVERTISING_DATA_PROPERTIES.add( # pylint: disable=protected-access + "RSSI" + ) + + +class BluetoothManager: + """Manage Bluetooth.""" + + def __init__( + self, + hass: HomeAssistant, + integration_matchers: list[BluetoothMatcher], + scanning_mode: BluetoothScanningMode, + ) -> None: + """Init bluetooth discovery.""" + self.hass = hass + self.scanning_mode = scanning_mode + self._integration_matchers = integration_matchers + self.scanner: HaBleakScanner | None = None + self._cancel_device_detected: CALLBACK_TYPE | None = None + self._callbacks: list[tuple[BluetoothCallback, BluetoothMatcher | None]] = [] + # Some devices use a random address so we need to use + # an LRU to avoid memory issues. + self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES) + + async def async_setup(self) -> None: + """Set up BT Discovery.""" + try: + self.scanner = HaBleakScanner( + scanning_mode=SCANNING_MODE_TO_BLEAK[self.scanning_mode] + ) + except (FileNotFoundError, BleakError) as ex: + _LOGGER.warning( + "Could not create bluetooth scanner (is bluetooth present and enabled?): %s", + ex, + ) + return + async_enable_rssi_updates() + install_multiple_bleak_catcher(self.scanner) + # We have to start it right away as some integrations might + # need it straight away. + _LOGGER.debug("Starting bluetooth scanner") + self.scanner.register_detection_callback(self.scanner.async_callback_dispatcher) + self._cancel_device_detected = self.scanner.async_register_callback( + self._device_detected, {} + ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + await self.scanner.start() + + @hass_callback + def _device_detected( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + matched_domains: set[str] | None = None + if device.address not in self._matched: + matched_domains = { + matcher["domain"] + for matcher in self._integration_matchers + if _ble_device_matches(matcher, device, advertisement_data) + } + if matched_domains: + self._matched[device.address] = True + _LOGGER.debug( + "Device detected: %s with advertisement_data: %s matched domains: %s", + device, + advertisement_data, + matched_domains, + ) + + if not matched_domains and not self._callbacks: + return + + service_info: BluetoothServiceInfo | None = None + for callback, matcher in self._callbacks: + if matcher is None or _ble_device_matches( + matcher, device, advertisement_data + ): + if service_info is None: + service_info = BluetoothServiceInfo.from_advertisement( + device, advertisement_data + ) + try: + callback(service_info, BluetoothChange.ADVERTISEMENT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + + if not matched_domains: + return + if service_info is None: + service_info = BluetoothServiceInfo.from_advertisement( + device, advertisement_data + ) + for domain in matched_domains: + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) + + @hass_callback + def async_register_callback( + self, callback: BluetoothCallback, match_dict: BluetoothMatcher | None = None + ) -> Callable[[], None]: + """Register a callback.""" + callback_entry = (callback, match_dict) + self._callbacks.append(callback_entry) + + @hass_callback + def _async_remove_callback() -> None: + self._callbacks.remove(callback_entry) + + return _async_remove_callback + + async def async_stop(self, event: Event) -> None: + """Stop bluetooth discovery.""" + if self._cancel_device_detected: + self._cancel_device_detected() + self._cancel_device_detected = None + if self.scanner: + await self.scanner.stop() + models.HA_BLEAK_SCANNER = None diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py new file mode 100644 index 00000000000..ca5777ccdc2 --- /dev/null +++ b/homeassistant/components/bluetooth/const.py @@ -0,0 +1,3 @@ +"""Constants for the Bluetooth integration.""" + +DOMAIN = "bluetooth" diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json new file mode 100644 index 00000000000..0cc11ee14b3 --- /dev/null +++ b/homeassistant/components/bluetooth/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "bluetooth", + "name": "Bluetooth", + "documentation": "https://www.home-assistant.io/integrations/bluetooth", + "dependencies": ["websocket_api"], + "quality_scale": "internal", + "requirements": ["bleak==0.14.3"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py new file mode 100644 index 00000000000..a2651c587f7 --- /dev/null +++ b/homeassistant/components/bluetooth/models.py @@ -0,0 +1,142 @@ +"""Models for bluetooth.""" +from __future__ import annotations + +import asyncio +import contextlib +import logging +from typing import Any, Final, cast + +from bleak import BleakScanner +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData, AdvertisementDataCallback +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant.core import CALLBACK_TYPE, callback as hass_callback + +_LOGGER = logging.getLogger(__name__) + +FILTER_UUIDS: Final = "UUIDs" + +HA_BLEAK_SCANNER: HaBleakScanner | None = None + +MAX_HISTORY_SIZE: Final = 512 + + +def _dispatch_callback( + callback: AdvertisementDataCallback, + filters: dict[str, set[str]], + device: BLEDevice, + advertisement_data: AdvertisementData, +) -> None: + """Dispatch the callback.""" + if not callback: + # Callback destroyed right before being called, ignore + return + + if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( + advertisement_data.service_uuids + ): + return + + try: + callback(device, advertisement_data) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in callback: %s", callback) + + +class HaBleakScanner(BleakScanner): # type: ignore[misc] + """BleakScanner that cannot be stopped.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the BleakScanner.""" + self._callbacks: list[ + tuple[AdvertisementDataCallback, dict[str, set[str]]] + ] = [] + self._history: LRU = LRU(MAX_HISTORY_SIZE) + super().__init__(*args, **kwargs) + + @hass_callback + def async_register_callback( + self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] + ) -> CALLBACK_TYPE: + """Register a callback.""" + callback_entry = (callback, filters) + self._callbacks.append(callback_entry) + + @hass_callback + def _remove_callback() -> None: + self._callbacks.remove(callback_entry) + + # Replay the history since otherwise we miss devices + # that were already discovered before the callback was registered + # or we are in passive mode + for device, advertisement_data in self._history.values(): + _dispatch_callback(callback, filters, device, advertisement_data) + + return _remove_callback + + def async_callback_dispatcher( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Dispatch the callback. + + Here we get the actual callback from bleak and dispatch + it to all the wrapped HaBleakScannerWrapper classes + """ + self._history[device.address] = (device, advertisement_data) + for callback_filters in self._callbacks: + _dispatch_callback(*callback_filters, device, advertisement_data) + + +class HaBleakScannerWrapper(BleakScanner): # type: ignore[misc] + """A wrapper that uses the single instance.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the BleakScanner.""" + self._detection_cancel: CALLBACK_TYPE | None = None + self._mapped_filters: dict[str, set[str]] = {} + if "filters" in kwargs: + self._mapped_filters = {k: set(v) for k, v in kwargs["filters"].items()} + if "service_uuids" in kwargs: + self._mapped_filters[FILTER_UUIDS] = set(kwargs["service_uuids"]) + super().__init__(*args, **kwargs) + + async def stop(self, *args: Any, **kwargs: Any) -> None: + """Stop scanning for devices.""" + return + + async def start(self, *args: Any, **kwargs: Any) -> None: + """Start scanning for devices.""" + return + + def _cancel_callback(self) -> None: + """Cancel callback.""" + if self._detection_cancel: + self._detection_cancel() + self._detection_cancel = None + + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + assert HA_BLEAK_SCANNER is not None + return cast(list[BLEDevice], HA_BLEAK_SCANNER.discovered_devices) + + def register_detection_callback(self, callback: AdvertisementDataCallback) -> None: + """Register a callback that is called when a device is discovered or has a property changed. + + This method takes the callback and registers it with the long running + scanner. + """ + self._cancel_callback() + super().register_detection_callback(callback) + assert HA_BLEAK_SCANNER is not None + self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( + self._callback, self._mapped_filters + ) + + def __del__(self) -> None: + """Delete the BleakScanner.""" + if self._detection_cancel: + # Nothing to do if event loop is already closed + with contextlib.suppress(RuntimeError): + asyncio.get_running_loop().call_soon_threadsafe(self._detection_cancel) diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py new file mode 100644 index 00000000000..e305576f97f --- /dev/null +++ b/homeassistant/components/bluetooth/usage.py @@ -0,0 +1,13 @@ +"""bluetooth usage utility to handle multiple instances.""" +from __future__ import annotations + +import bleak + +from . import models +from .models import HaBleakScanner, HaBleakScannerWrapper + + +def install_multiple_bleak_catcher(hass_bleak_scanner: HaBleakScanner) -> None: + """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" + models.HA_BLEAK_SCANNER = hass_bleak_scanner + bleak.BleakScanner = HaBleakScannerWrapper diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index cb485ffd8a5..91ee8881a07 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -5,6 +5,7 @@ "requirements": ["PySwitchbot==0.14.0"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], + "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], "iot_class": "local_polling", "loggers": ["switchbot"] } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c832bab7eb4..e7f65c38ec1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -28,6 +28,7 @@ from .util import uuid as uuid_util from .util.decorator import Registry if TYPE_CHECKING: + from .components.bluetooth import BluetoothServiceInfo from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo from .components.mqtt import MqttServiceInfo @@ -37,6 +38,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +SOURCE_BLUETOOTH = "bluetooth" SOURCE_DHCP = "dhcp" SOURCE_DISCOVERY = "discovery" SOURCE_HASSIO = "hassio" @@ -116,6 +118,7 @@ class ConfigEntryState(Enum): DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id" DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" DISCOVERY_SOURCES = { + SOURCE_BLUETOOTH, SOURCE_DHCP, SOURCE_DISCOVERY, SOURCE_HOMEKIT, @@ -1460,6 +1463,12 @@ class ConfigFlow(data_entry_flow.FlowHandler): reason=reason, description_placeholders=description_placeholders ) + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by Bluetooth discovery.""" + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + async def async_step_dhcp( self, discovery_info: DhcpServiceInfo ) -> data_entry_flow.FlowResult: diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py new file mode 100644 index 00000000000..49596c4773c --- /dev/null +++ b/homeassistant/generated/bluetooth.py @@ -0,0 +1,14 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +""" +from __future__ import annotations + +# fmt: off + +BLUETOOTH: list[dict[str, str | int]] = [ + { + "domain": "switchbot", + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + } +] diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 3617c0b1f29..75a4dcd20f4 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import dhcp, onboarding, ssdp, zeroconf +from homeassistant.components import bluetooth, dhcp, onboarding, ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -92,6 +92,17 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() + async def async_step_bluetooth( + self, discovery_info: bluetooth.BluetoothServiceInfo + ) -> FlowResult: + """Handle a flow initialized by bluetooth discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(self._domain) + + return await self.async_step_confirm() + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle a flow initialized by dhcp discovery.""" if self._async_in_progress() or self._async_current_entries(): diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ab681d7c42d..0a65928701b 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -24,6 +24,7 @@ from awesomeversion import ( ) from .generated.application_credentials import APPLICATION_CREDENTIALS +from .generated.bluetooth import BLUETOOTH from .generated.dhcp import DHCP from .generated.mqtt import MQTT from .generated.ssdp import SSDP @@ -77,6 +78,25 @@ class DHCPMatcher(DHCPMatcherRequired, DHCPMatcherOptional): """Matcher for the dhcp integration.""" +class BluetoothMatcherRequired(TypedDict, total=True): + """Matcher for the bluetooth integration for required fields.""" + + domain: str + + +class BluetoothMatcherOptional(TypedDict, total=False): + """Matcher for the bluetooth integration for optional fields.""" + + local_name: str + service_uuid: str + manufacturer_id: int + manufacturer_data_first_byte: int + + +class BluetoothMatcher(BluetoothMatcherRequired, BluetoothMatcherOptional): + """Matcher for the bluetooth integration.""" + + class Manifest(TypedDict, total=False): """ Integration manifest. @@ -97,6 +117,7 @@ class Manifest(TypedDict, total=False): issue_tracker: str quality_scale: str iot_class: str + bluetooth: list[dict[str, int | str]] mqtt: list[str] ssdp: list[dict[str, str]] zeroconf: list[str | dict[str, str]] @@ -269,6 +290,22 @@ async def async_get_zeroconf( return zeroconf +async def async_get_bluetooth(hass: HomeAssistant) -> list[BluetoothMatcher]: + """Return cached list of bluetooth types.""" + bluetooth = cast(list[BluetoothMatcher], BLUETOOTH.copy()) + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if not integration.bluetooth: + continue + for entry in integration.bluetooth: + bluetooth.append( + cast(BluetoothMatcher, {"domain": integration.domain, **entry}) + ) + + return bluetooth + + async def async_get_dhcp(hass: HomeAssistant) -> list[DHCPMatcher]: """Return cached list of dhcp types.""" dhcp = cast(list[DHCPMatcher], DHCP.copy()) @@ -519,6 +556,11 @@ class Integration: """Return Integration zeroconf entries.""" return self.manifest.get("zeroconf") + @property + def bluetooth(self) -> list[dict[str, str | int]] | None: + """Return Integration bluetooth entries.""" + return self.manifest.get("bluetooth") + @property def dhcp(self) -> list[dict[str, str | bool]] | None: """Return Integration dhcp entries.""" diff --git a/mypy.ini b/mypy.ini index c47413b4af8..a883007de0d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -390,6 +390,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.bluetooth.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.bluetooth_tracker.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index d4e792ee233..766ceea6e07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -401,6 +401,9 @@ bimmer_connected==0.9.6 # homeassistant.components.bizkaibus bizkaibus==0.1.1 +# homeassistant.components.bluetooth +bleak==0.14.3 + # homeassistant.components.blebox blebox_uniapi==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bc006c0b54..30cbba02ff4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -316,6 +316,9 @@ bellows==0.31.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 +# homeassistant.components.bluetooth +bleak==0.14.3 + # homeassistant.components.blebox blebox_uniapi==2.0.1 diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 889cad2a497..4bc30583d47 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -6,6 +6,7 @@ from time import monotonic from . import ( application_credentials, + bluetooth, codeowners, config_flow, coverage, @@ -27,6 +28,7 @@ from .model import Config, Integration INTEGRATION_PLUGINS = [ application_credentials, + bluetooth, codeowners, config_flow, dependencies, diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py new file mode 100644 index 00000000000..77a8779efbd --- /dev/null +++ b/script/hassfest/bluetooth.py @@ -0,0 +1,65 @@ +"""Generate bluetooth file.""" +from __future__ import annotations + +import json + +from .model import Config, Integration + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +\"\"\" +from __future__ import annotations + +# fmt: off + +BLUETOOTH: list[dict[str, str | int]] = {} +""".strip() + + +def generate_and_validate(integrations: list[dict[str, str]]): + """Validate and generate bluetooth data.""" + match_list = [] + + for domain in sorted(integrations): + integration = integrations[domain] + + if not integration.manifest or not integration.config_flow: + continue + + match_types = integration.manifest.get("bluetooth", []) + + if not match_types: + continue + + for entry in match_types: + match_list.append({"domain": domain, **entry}) + + return BASE.format(json.dumps(match_list, indent=4)) + + +def validate(integrations: dict[str, Integration], config: Config): + """Validate bluetooth file.""" + bluetooth_path = config.root / "homeassistant/generated/bluetooth.py" + config.cache["bluetooth"] = content = generate_and_validate(integrations) + + if config.specific_integrations: + return + + with open(str(bluetooth_path)) as fp: + current = fp.read().strip() + if current != content: + config.add_error( + "bluetooth", + "File bluetooth.py is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + return + + +def generate(integrations: dict[str, Integration], config: Config): + """Generate bluetooth file.""" + bluetooth_path = config.root / "homeassistant/generated/bluetooth.py" + with open(str(bluetooth_path), "w") as fp: + fp.write(f"{config.cache['bluetooth']}\n") diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 169ccedf4a1..ad4a1d79229 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -35,6 +35,7 @@ def validate_integration(config: Config, integration: Integration): needs_unique_id = integration.domain not in UNIQUE_ID_IGNORE and ( "async_step_discovery" in config_flow + or "async_step_bluetooth" in config_flow or "async_step_hassio" in config_flow or "async_step_homekit" in config_flow or "async_step_mqtt" in config_flow diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index b847d384361..4f76fb9ed1e 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -190,6 +190,16 @@ MANIFEST_SCHEMA = vol.Schema( vol.Optional("ssdp"): vol.Schema( vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), + vol.Optional("bluetooth"): [ + vol.Schema( + { + vol.Optional("service_uuid"): vol.All(str, verify_lowercase), + vol.Optional("local_name"): vol.All(str), + vol.Optional("manufacturer_id"): int, + vol.Optional("manufacturer_data_first_byte"): int, + } + ) + ], vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}), vol.Optional("dhcp"): [ vol.Schema( diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py new file mode 100644 index 00000000000..6bf53afcd1e --- /dev/null +++ b/tests/components/bluetooth/__init__.py @@ -0,0 +1 @@ +"""Tests for the Bluetooth integration.""" diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py new file mode 100644 index 00000000000..fc0bd85b795 --- /dev/null +++ b/tests/components/bluetooth/conftest.py @@ -0,0 +1,25 @@ +"""Tests for the bluetooth component.""" + +import threading + +import pytest + +from tests.common import INSTANCES + + +@pytest.fixture(autouse=True) +def verify_cleanup(): + """Verify that the test has cleaned up resources correctly.""" + threads_before = frozenset(threading.enumerate()) + + yield + + if len(INSTANCES) >= 2: + count = len(INSTANCES) + for inst in INSTANCES: + inst.stop() + pytest.exit(f"Detected non stopped instances ({count}), aborting test run") + + threads = frozenset(threading.enumerate()) - threads_before + for thread in threads: + assert isinstance(thread, threading._DummyThread) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py new file mode 100644 index 00000000000..1e0647df01c --- /dev/null +++ b/tests/components/bluetooth/test_init.py @@ -0,0 +1,440 @@ +"""Tests for the Bluetooth integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +import bleak +from bleak import BleakError +from bleak.backends.scanner import AdvertisementData, BLEDevice +import pytest + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import ( + BluetoothChange, + BluetoothServiceInfo, + models, +) +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component + + +@pytest.fixture() +def mock_bleak_scanner_start(): + """Fixture to mock starting the bleak scanner.""" + scanner = bleak.BleakScanner + models.HA_BLEAK_SCANNER = None + + with patch("homeassistant.components.bluetooth.HaBleakScanner.stop"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ) as mock_bleak_scanner_start: + yield mock_bleak_scanner_start + + # We need to drop the stop method from the object since we patched + # out start and this fixture will expire before the stop method is called + # when EVENT_HOMEASSISTANT_STOP is fired. + if models.HA_BLEAK_SCANNER: + models.HA_BLEAK_SCANNER.stop = AsyncMock() + bleak.BleakScanner = scanner + + +async def test_setup_and_stop(hass, mock_bleak_scanner_start): + """Test we and setup and stop the scanner.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + +async def test_setup_and_stop_no_bluetooth(hass, caplog): + """Test we fail gracefully when bluetooth is not available.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError + ) as mock_ha_bleak_scanner, patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_ha_bleak_scanner.mock_calls) == 1 + assert "Could not create bluetooth scanner" in caplog.text + + +async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): + """Test bluetooth discovery match by service_uuid.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + + models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "switchbot" + + +async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): + """Test bluetooth discovery match by local_name.""" + mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + + models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "switchbot" + + +async def test_discovery_match_by_manufacturer_id_and_first_byte( + hass, mock_bleak_scanner_start +): + """Test bluetooth discovery match by manufacturer_id and manufacturer_data_first_byte.""" + mock_bt = [ + { + "domain": "homekit_controller", + "manufacturer_id": 76, + "manufacturer_data_first_byte": 0x06, + } + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + hkc_device = BLEDevice("44:44:33:11:23:45", "lock") + hkc_adv = AdvertisementData( + local_name="lock", service_uuids=[], manufacturer_data={76: b"\x06"} + ) + + models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller" + mock_config_flow.reset_mock() + + # 2nd discovery should not generate another flow + models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + mock_config_flow.reset_mock() + not_hkc_device = BLEDevice("44:44:33:11:23:21", "lock") + not_hkc_adv = AdvertisementData( + local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} + ) + + models.HA_BLEAK_SCANNER._callback(not_hkc_device, not_hkc_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + not_apple_device = BLEDevice("44:44:33:11:23:23", "lock") + not_apple_adv = AdvertisementData( + local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} + ) + + models.HA_BLEAK_SCANNER._callback(not_apple_device, not_apple_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + +async def test_register_callbacks(hass, mock_bleak_scanner_start): + """Test configured options for a device are loaded via config entry.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + if len(callbacks) >= 3: + raise ValueError + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"service_uuids": {"cba20d00-224d-11e6-9fb8-0002a5d5c51b"}}, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + # 3rd callback raises ValueError but is still tracked + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + # 4th callback should not be tracked since we canceled + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + assert len(callbacks) == 3 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + service_info: BluetoothServiceInfo = callbacks[1][0] + assert service_info.name == "empty" + assert service_info.manufacturer is None + assert service_info.manufacturer_id is None + + service_info: BluetoothServiceInfo = callbacks[2][0] + assert service_info.name == "empty" + assert service_info.manufacturer is None + assert service_info.manufacturer_id is None + + +async def test_wrapped_instance_with_filter(hass, mock_bleak_scanner_start): + """Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) + + mock_discovered = [MagicMock()] + type(models.HA_BLEAK_SCANNER).discovered_devices = mock_discovered + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 1 + assert discovered == mock_discovered + assert len(detected) == 1 + + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 2 + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 3 + + type(models.HA_BLEAK_SCANNER).discovered_devices = [] + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 0 + assert discovered == [] + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + assert len(detected) == 4 + + # The filter we created in the wrapped scanner with should be respected + # and we should not get another callback + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + assert len(detected) == 4 + + +async def test_wrapped_instance_with_service_uuids(hass, mock_bleak_scanner_start): + """Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) + + type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] + for _ in range(2): + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + assert len(detected) == 2 + + +async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_start): + """Test broken callbacks do not cause the scanner to fail.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + if detected: + raise ValueError + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + assert len(detected) == 1 diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py new file mode 100644 index 00000000000..92339735340 --- /dev/null +++ b/tests/components/bluetooth/test_usage.py @@ -0,0 +1,22 @@ +"""Tests for the Bluetooth integration.""" + +from unittest.mock import MagicMock + +import bleak + +from homeassistant.components.bluetooth import models +from homeassistant.components.bluetooth.models import HaBleakScannerWrapper +from homeassistant.components.bluetooth.usage import install_multiple_bleak_catcher + + +async def test_multiple_bleak_scanner_instances(hass): + """Test creating multiple zeroconf throws without an integration.""" + assert models.HA_BLEAK_SCANNER is None + mock_scanner = MagicMock() + + install_multiple_bleak_catcher(mock_scanner) + + instance = bleak.BleakScanner() + + assert isinstance(instance, HaBleakScannerWrapper) + assert models.HA_BLEAK_SCANNER is mock_scanner diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index dcb0ee7abfd..63fa58851e3 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -94,6 +94,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): @pytest.mark.parametrize( "source", [ + config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, @@ -117,6 +118,7 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): @pytest.mark.parametrize( "source", [ + config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, @@ -142,6 +144,7 @@ async def test_discovery_confirmation(hass, discovery_flow_conf, source): @pytest.mark.parametrize( "source", [ + config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1970d883efc..3e7245ed73a 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2497,6 +2497,7 @@ async def test_async_setup_update_entry(hass): @pytest.mark.parametrize( "discovery_source", ( + (config_entries.SOURCE_BLUETOOTH, BaseServiceInfo()), (config_entries.SOURCE_DISCOVERY, {}), (config_entries.SOURCE_SSDP, BaseServiceInfo()), (config_entries.SOURCE_USB, BaseServiceInfo()), diff --git a/tests/test_loader.py b/tests/test_loader.py index 96694c43c7f..b8a469f80aa 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -205,6 +205,7 @@ def test_integration_properties(hass): {"hostname": "tesla_*", "macaddress": "98ED5C*"}, {"registered_devices": True}, ], + "bluetooth": [{"manufacturer_id": 76, "manufacturer_data_first_byte": 6}], "usb": [ {"vid": "10C4", "pid": "EA60"}, {"vid": "1CF1", "pid": "0030"}, @@ -242,6 +243,9 @@ def test_integration_properties(hass): {"vid": "1A86", "pid": "7523"}, {"vid": "10C4", "pid": "8A2A"}, ] + assert integration.bluetooth == [ + {"manufacturer_id": 76, "manufacturer_data_first_byte": 6} + ] assert integration.ssdp == [ { "manufacturer": "Royal Philips Electronics", @@ -274,6 +278,7 @@ def test_integration_properties(hass): assert integration.homekit is None assert integration.zeroconf is None assert integration.dhcp is None + assert integration.bluetooth is None assert integration.usb is None assert integration.ssdp is None assert integration.mqtt is None @@ -296,6 +301,7 @@ def test_integration_properties(hass): assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}] assert integration.dhcp is None assert integration.usb is None + assert integration.bluetooth is None assert integration.ssdp is None @@ -417,6 +423,25 @@ def _get_test_integration_with_dhcp_matcher(hass, name, config_flow): ) +def _get_test_integration_with_bluetooth_matcher(hass, name, config_flow): + """Return a generated test integration with a bluetooth matcher.""" + return loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": config_flow, + "bluetooth": [ + { + "local_name": "Prodigio_*", + }, + ], + }, + ) + + def _get_test_integration_with_usb_matcher(hass, name, config_flow): """Return a generated test integration with a usb matcher.""" return loader.Integration( @@ -543,6 +568,26 @@ async def test_get_zeroconf_back_compat(hass): ] +async def test_get_bluetooth(hass): + """Verify that custom components with bluetooth are found.""" + test_1_integration = _get_test_integration_with_bluetooth_matcher( + hass, "test_1", True + ) + test_2_integration = _get_test_integration_with_dhcp_matcher(hass, "test_2", True) + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = { + "test_1": test_1_integration, + "test_2": test_2_integration, + } + bluetooth = await loader.async_get_bluetooth(hass) + bluetooth_for_domain = [ + entry for entry in bluetooth if entry["domain"] == "test_1" + ] + assert bluetooth_for_domain == [ + {"domain": "test_1", "local_name": "Prodigio_*"}, + ] + + async def test_get_dhcp(hass): """Verify that custom components with dhcp are found.""" test_1_integration = _get_test_integration_with_dhcp_matcher(hass, "test_1", True) From cdaefc8fdaa260d03e6b6a6580858171a20a4b52 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 9 Jul 2022 00:20:23 +0000 Subject: [PATCH 231/820] [ci skip] Translation update --- .../components/airly/translations/ja.json | 2 +- .../components/anthemav/translations/id.json | 7 ++++++- .../application_credentials/translations/cs.json | 3 +++ .../components/awair/translations/id.json | 4 ++++ .../components/binary_sensor/translations/cs.json | 4 ++++ .../components/bosch_shc/translations/cs.json | 1 + .../components/deconz/translations/cs.json | 1 + .../components/esphome/translations/id.json | 4 ++-- .../components/esphome/translations/ja.json | 2 +- .../components/fibaro/translations/cs.json | 7 +++++++ .../components/fritz/translations/cs.json | 1 + .../garages_amsterdam/translations/cs.json | 1 + .../components/generic/translations/cs.json | 3 +++ .../components/generic/translations/id.json | 2 ++ .../google_travel_time/translations/cs.json | 1 + .../here_travel_time/translations/ca.json | 7 +++++++ .../here_travel_time/translations/de.json | 9 ++++++++- .../here_travel_time/translations/el.json | 7 +++++++ .../here_travel_time/translations/et.json | 7 +++++++ .../here_travel_time/translations/fr.json | 7 +++++++ .../here_travel_time/translations/id.json | 7 +++++++ .../here_travel_time/translations/pt-BR.json | 7 +++++++ homeassistant/components/hive/translations/ja.json | 2 +- homeassistant/components/hue/translations/bg.json | 1 + .../components/insteon/translations/bg.json | 5 +++++ .../components/isy994/translations/cs.json | 1 + .../components/lg_soundbar/translations/id.json | 6 +++++- .../components/life360/translations/id.json | 6 ++++++ .../components/media_player/translations/cs.json | 4 ++++ .../components/mjpeg/translations/cs.json | 3 +++ .../components/motioneye/translations/bg.json | 3 ++- homeassistant/components/nam/translations/cs.json | 1 + .../components/nextdns/translations/id.json | 4 +++- homeassistant/components/nina/translations/id.json | 9 +++++++-- .../components/onewire/translations/ja.json | 2 +- .../components/qnap_qsw/translations/cs.json | 7 +++++++ .../components/qnap_qsw/translations/id.json | 6 ++++++ .../components/ridwell/translations/cs.json | 1 + .../components/select/translations/cs.json | 8 ++++++++ .../components/sensor/translations/cs.json | 2 ++ .../components/skybell/translations/cs.json | 7 +++++++ .../components/solax/translations/cs.json | 1 + .../components/soundtouch/translations/id.json | 5 +++++ .../components/spotify/translations/ja.json | 2 +- .../components/steam_online/translations/cs.json | 14 ++++++++++++++ .../components/syncthing/translations/cs.json | 1 + .../components/ukraine_alarm/translations/ja.json | 2 +- .../components/version/translations/cs.json | 5 +++++ 48 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/application_credentials/translations/cs.json create mode 100644 homeassistant/components/fibaro/translations/cs.json create mode 100644 homeassistant/components/qnap_qsw/translations/cs.json create mode 100644 homeassistant/components/select/translations/cs.json create mode 100644 homeassistant/components/skybell/translations/cs.json create mode 100644 homeassistant/components/steam_online/translations/cs.json diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 5116cac487a..fbc64fa69ea 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -15,7 +15,7 @@ "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, - "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/anthemav/translations/id.json b/homeassistant/components/anthemav/translations/id.json index d6bac04f480..1eb2ba0b5a1 100644 --- a/homeassistant/components/anthemav/translations/id.json +++ b/homeassistant/components/anthemav/translations/id.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "cannot_receive_deviceinfo": "Gagal mengambil alamat MAC. Pastikan perangkat nyala" }, "step": { "user": { "data": { + "host": "Host", "port": "Port" } } diff --git a/homeassistant/components/application_credentials/translations/cs.json b/homeassistant/components/application_credentials/translations/cs.json new file mode 100644 index 00000000000..6699bb0a908 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "P\u0159ihla\u0161ovac\u00ed \u00fadaje aplikace" +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index ab9431a6a46..863b7982b2a 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -18,6 +18,10 @@ "description": "Masukkan kembali token akses pengembang Awair Anda." }, "reauth_confirm": { + "data": { + "access_token": "Token Akses", + "email": "Email" + }, "description": "Masukkan kembali token akses pengembang Awair Anda." }, "user": { diff --git a/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant/components/binary_sensor/translations/cs.json index b8793a2c087..9e5c5b9d7c2 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -97,6 +97,10 @@ "vibration": "{entity_name} za\u010dalo detekovat vibrace" } }, + "device_class": { + "gas": "plyn", + "sound": "zvuk" + }, "state": { "_": { "off": "Neaktivn\u00ed", diff --git a/homeassistant/components/bosch_shc/translations/cs.json b/homeassistant/components/bosch_shc/translations/cs.json index 72df4a96818..45e02001105 100644 --- a/homeassistant/components/bosch_shc/translations/cs.json +++ b/homeassistant/components/bosch_shc/translations/cs.json @@ -4,6 +4,7 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 8e6c6a71a44..b8afd28926c 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -68,6 +68,7 @@ "remote_button_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku", "remote_button_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", "remote_button_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t", + "remote_button_rotated": "Tla\u010d\u00edtko \"{subtype}\" se oto\u010dilo", "remote_button_rotation_stopped": "Oto\u010den\u00ed tla\u010d\u00edtka \"{subtype}\" bylo zastaveno", "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index e099405bf0d..155bf33d986 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -9,7 +9,7 @@ "connection_error": "Tidak dapat terhubung ke ESP. Pastikan file YAML Anda mengandung baris 'api:'.", "invalid_auth": "Autentikasi tidak valid", "invalid_psk": "Kunci enkripsi transport tidak valid. Pastikan kuncinya sesuai dengan yang ada pada konfigurasi Anda", - "resolve_error": "Tidak dapat menemukan alamat ESP. Jika kesalahan ini terus terjadi, atur alamat IP statis: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Tidak dapat menemukan alamat ESP. Jika kesalahan ini terus terjadi, atur alamat IP statis" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Port" }, - "description": "Masukkan pengaturan koneksi node [ESPHome](https://esphomelib.com/)." + "description": "Masukkan pengaturan koneksi node [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json index 0779d072b94..a4bfbc327dd 100644 --- a/homeassistant/components/esphome/translations/ja.json +++ b/homeassistant/components/esphome/translations/ja.json @@ -9,7 +9,7 @@ "connection_error": "ESP\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002YAML\u30d5\u30a1\u30a4\u30eb\u306b 'api:' \u306e\u884c\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_psk": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u306e\u6697\u53f7\u5316\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059\u3002\u8a2d\u5b9a\u3068\u4e00\u81f4\u3057\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "resolve_error": "ESP\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u9759\u7684\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "ESP\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001\u56fa\u5b9aIP\u30a2\u30c9\u30ec\u30b9\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fibaro/translations/cs.json b/homeassistant/components/fibaro/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/fibaro/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/cs.json b/homeassistant/components/fritz/translations/cs.json index 55326bb1803..8f114045caf 100644 --- a/homeassistant/components/fritz/translations/cs.json +++ b/homeassistant/components/fritz/translations/cs.json @@ -10,6 +10,7 @@ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/garages_amsterdam/translations/cs.json b/homeassistant/components/garages_amsterdam/translations/cs.json index 3b814303e69..5073c9248e0 100644 --- a/homeassistant/components/garages_amsterdam/translations/cs.json +++ b/homeassistant/components/garages_amsterdam/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } } diff --git a/homeassistant/components/generic/translations/cs.json b/homeassistant/components/generic/translations/cs.json index 520e0743edd..8ef4333b872 100644 --- a/homeassistant/components/generic/translations/cs.json +++ b/homeassistant/components/generic/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, "step": { "content_type": { "data": { diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 77ea5f01f0b..5222c111c58 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -9,6 +9,7 @@ "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", + "relative_url": "URL relatif tidak diizinkan", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", @@ -53,6 +54,7 @@ "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", + "relative_url": "URL relatif tidak diizinkan", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", diff --git a/homeassistant/components/google_travel_time/translations/cs.json b/homeassistant/components/google_travel_time/translations/cs.json index a6c3b361960..19da83d1596 100644 --- a/homeassistant/components/google_travel_time/translations/cs.json +++ b/homeassistant/components/google_travel_time/translations/cs.json @@ -19,6 +19,7 @@ "step": { "init": { "data": { + "language": "Jazyk", "time": "\u010cas", "units": "Jednotky" } diff --git a/homeassistant/components/here_travel_time/translations/ca.json b/homeassistant/components/here_travel_time/translations/ca.json index 08dfb0d970d..586e5bd5442 100644 --- a/homeassistant/components/here_travel_time/translations/ca.json +++ b/homeassistant/components/here_travel_time/translations/ca.json @@ -39,6 +39,13 @@ }, "title": "Tria origen" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Utilitzant una ubicaci\u00f3 de mapa", + "origin_entity": "Utilitzant una entitat" + }, + "title": "Tria l'origen" + }, "user": { "data": { "api_key": "Clau API", diff --git a/homeassistant/components/here_travel_time/translations/de.json b/homeassistant/components/here_travel_time/translations/de.json index b50ef028089..db93b79fd2f 100644 --- a/homeassistant/components/here_travel_time/translations/de.json +++ b/homeassistant/components/here_travel_time/translations/de.json @@ -23,7 +23,7 @@ "destination_menu": { "menu_options": { "destination_coordinates": "Verwendung einer Kartenposition", - "destination_entity": "Verwenden einer Entit\u00e4t" + "destination_entity": "Verwendung einer Entit\u00e4t" }, "title": "Ziel w\u00e4hlen" }, @@ -39,6 +39,13 @@ }, "title": "Herkunft w\u00e4hlen" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Verwendung einer Kartenposition", + "origin_entity": "Verwendung einer Entit\u00e4t" + }, + "title": "Herkunft w\u00e4hlen" + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", diff --git a/homeassistant/components/here_travel_time/translations/el.json b/homeassistant/components/here_travel_time/translations/el.json index cd4d986ed3f..062cf89ea0f 100644 --- a/homeassistant/components/here_travel_time/translations/el.json +++ b/homeassistant/components/here_travel_time/translations/el.json @@ -39,6 +39,13 @@ }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b8\u03ad\u03c3\u03b7\u03c2 \u03c7\u03ac\u03c1\u03c4\u03b7", + "origin_entity": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2" + }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", diff --git a/homeassistant/components/here_travel_time/translations/et.json b/homeassistant/components/here_travel_time/translations/et.json index c9646af7544..a017971a33f 100644 --- a/homeassistant/components/here_travel_time/translations/et.json +++ b/homeassistant/components/here_travel_time/translations/et.json @@ -39,6 +39,13 @@ }, "title": "Vali l\u00e4htekoht" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Kasuta asukohta kaardil", + "origin_entity": "Kasuta olemi andmeid" + }, + "title": "Vali l\u00e4htepunkt" + }, "user": { "data": { "api_key": "API v\u00f5ti", diff --git a/homeassistant/components/here_travel_time/translations/fr.json b/homeassistant/components/here_travel_time/translations/fr.json index 063a4008779..c44c2337af9 100644 --- a/homeassistant/components/here_travel_time/translations/fr.json +++ b/homeassistant/components/here_travel_time/translations/fr.json @@ -39,6 +39,13 @@ }, "title": "Choix de l'origine" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Utilisation d'un emplacement sur la carte", + "origin_entity": "Utilisation d'une entit\u00e9" + }, + "title": "Choix de l'origine" + }, "user": { "data": { "api_key": "Cl\u00e9 d'API", diff --git a/homeassistant/components/here_travel_time/translations/id.json b/homeassistant/components/here_travel_time/translations/id.json index f03910d9adf..57d4ec5ee80 100644 --- a/homeassistant/components/here_travel_time/translations/id.json +++ b/homeassistant/components/here_travel_time/translations/id.json @@ -39,6 +39,13 @@ }, "title": "Pilih Asal" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Menggunakan lokasi pada peta", + "origin_entity": "Menggunakan entitas" + }, + "title": "Pilih Asal" + }, "user": { "data": { "api_key": "Kunci API", diff --git a/homeassistant/components/here_travel_time/translations/pt-BR.json b/homeassistant/components/here_travel_time/translations/pt-BR.json index 34f862f0029..9b524df7539 100644 --- a/homeassistant/components/here_travel_time/translations/pt-BR.json +++ b/homeassistant/components/here_travel_time/translations/pt-BR.json @@ -39,6 +39,13 @@ }, "title": "Escolha a Origem" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Usando uma localiza\u00e7\u00e3o no mapa", + "origin_entity": "Usando uma entidade" + }, + "title": "Escolha a Origem" + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index ed11bbd8b7e..981c65ba540 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -41,7 +41,7 @@ "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "Hive\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3068\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "description": "Hive\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "Hive\u30ed\u30b0\u30a4\u30f3" } } diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index 68da5279508..93617d8c0e5 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -39,6 +39,7 @@ "2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "double_buttons_1_3": "\u041f\u044a\u0440\u0432\u0438 \u0438 \u0442\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "double_buttons_2_4": "\u0412\u0442\u043e\u0440\u0438 \u0438 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438" } diff --git a/homeassistant/components/insteon/translations/bg.json b/homeassistant/components/insteon/translations/bg.json index e1adb8657da..f3fb9df607c 100644 --- a/homeassistant/components/insteon/translations/bg.json +++ b/homeassistant/components/insteon/translations/bg.json @@ -34,6 +34,11 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "add_x10": { + "data": { + "platform": "\u041f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430" + } + }, "change_hub_config": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json index e2d3dc4c883..ac6773f09e1 100644 --- a/homeassistant/components/isy994/translations/cs.json +++ b/homeassistant/components/isy994/translations/cs.json @@ -7,6 +7,7 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_host": "Z\u00e1znam hostitele nebyl v \u00fapln\u00e9m form\u00e1tu URL, nap\u0159. http://192.168.10.100:80", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/lg_soundbar/translations/id.json b/homeassistant/components/lg_soundbar/translations/id.json index 70ce3eb75e9..74d380f3a1a 100644 --- a/homeassistant/components/lg_soundbar/translations/id.json +++ b/homeassistant/components/lg_soundbar/translations/id.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "existing_instance_updated": "Memperbarui konfigurasi yang ada." + }, + "error": { + "cannot_connect": "Gagal terhubung" }, "step": { "user": { diff --git a/homeassistant/components/life360/translations/id.json b/homeassistant/components/life360/translations/id.json index b219745572d..dfcdf97f46f 100644 --- a/homeassistant/components/life360/translations/id.json +++ b/homeassistant/components/life360/translations/id.json @@ -17,6 +17,12 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/media_player/translations/cs.json b/homeassistant/components/media_player/translations/cs.json index e20ec750f79..19e88635f8b 100644 --- a/homeassistant/components/media_player/translations/cs.json +++ b/homeassistant/components/media_player/translations/cs.json @@ -6,6 +6,10 @@ "is_on": "{entity_name} je zapnuto", "is_paused": "{entity_name} je pozastaven", "is_playing": "{entity_name} p\u0159ehr\u00e1v\u00e1" + }, + "trigger_type": { + "paused": "{entity_name} je pozastaveno", + "turned_on": "{entity_name} bylo zapnuto" } }, "state": { diff --git a/homeassistant/components/mjpeg/translations/cs.json b/homeassistant/components/mjpeg/translations/cs.json index 00616fbdd50..3fc5acfe8dd 100644 --- a/homeassistant/components/mjpeg/translations/cs.json +++ b/homeassistant/components/mjpeg/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, diff --git a/homeassistant/components/motioneye/translations/bg.json b/homeassistant/components/motioneye/translations/bg.json index ef56ca3f2df..d2db5257b51 100644 --- a/homeassistant/components/motioneye/translations/bg.json +++ b/homeassistant/components/motioneye/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d URL" }, "step": { "user": { diff --git a/homeassistant/components/nam/translations/cs.json b/homeassistant/components/nam/translations/cs.json index 72df4a96818..1b979ab1412 100644 --- a/homeassistant/components/nam/translations/cs.json +++ b/homeassistant/components/nam/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/nextdns/translations/id.json b/homeassistant/components/nextdns/translations/id.json index 9ebb40ae2d8..39f87faf813 100644 --- a/homeassistant/components/nextdns/translations/id.json +++ b/homeassistant/components/nextdns/translations/id.json @@ -4,7 +4,9 @@ "already_configured": "Profil NextDNS ini sudah dikonfigurasi." }, "error": { - "invalid_api_key": "Kunci API tidak valid" + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "profiles": { diff --git a/homeassistant/components/nina/translations/id.json b/homeassistant/components/nina/translations/id.json index 7e3642a04ef..f827ef935f7 100644 --- a/homeassistant/components/nina/translations/id.json +++ b/homeassistant/components/nina/translations/id.json @@ -26,7 +26,9 @@ }, "options": { "error": { - "no_selection": "Pilih setidaknya satu kota/kabupaten" + "cannot_connect": "Gagal terhubung", + "no_selection": "Pilih setidaknya satu kota/kabupaten", + "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "init": { @@ -35,7 +37,10 @@ "_e_to_h": "Kota/kabupaten (E-H)", "_i_to_l": "Kota/kabupaten (I-L)", "_m_to_q": "Kota/kabupaten (M-Q)", - "_r_to_u": "Kota/kabupaten (R-U)" + "_r_to_u": "Kota/kabupaten (R-U)", + "_v_to_z": "Kota/kabupaten (V-Z)", + "corona_filter": "Hapus Peringatan Corona", + "slots": "Peringatan maksimum per kota/kabupaten" }, "title": "Opsi" } diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 75b01a0cbc9..04e6bbac0cf 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -12,7 +12,7 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" }, - "title": "1-Wire\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "\u30b5\u30fc\u30d0\u30fc\u306e\u8a73\u7d30\u3092\u8a2d\u5b9a\u3059\u308b" } } }, diff --git a/homeassistant/components/qnap_qsw/translations/cs.json b/homeassistant/components/qnap_qsw/translations/cs.json new file mode 100644 index 00000000000..33006d6761b --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/id.json b/homeassistant/components/qnap_qsw/translations/id.json index b34bcd046d9..b9f124a765c 100644 --- a/homeassistant/components/qnap_qsw/translations/id.json +++ b/homeassistant/components/qnap_qsw/translations/id.json @@ -9,6 +9,12 @@ "invalid_auth": "Autentikasi tidak valid" }, "step": { + "discovered_connection": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/ridwell/translations/cs.json b/homeassistant/components/ridwell/translations/cs.json index 72df4a96818..5d43feee500 100644 --- a/homeassistant/components/ridwell/translations/cs.json +++ b/homeassistant/components/ridwell/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/select/translations/cs.json b/homeassistant/components/select/translations/cs.json new file mode 100644 index 00000000000..32d6cadee9e --- /dev/null +++ b/homeassistant/components/select/translations/cs.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Zm\u011bnit mo\u017enost {entity_name}" + } + }, + "title": "V\u00fdb\u011br" +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index f493f4134ed..f6f2e97fdd4 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -11,6 +11,7 @@ "is_power_factor": "Aktu\u00e1ln\u00ed \u00fa\u010din\u00edk {entity_name}", "is_pressure": "Aktu\u00e1ln\u00ed tlak {entity_name}", "is_signal_strength": "Aktu\u00e1ln\u00ed s\u00edla sign\u00e1lu {entity_name}", + "is_sulphur_dioxide": "Aktu\u00e1ln\u00ed \u00farove\u0148 koncentrace oxidu si\u0159i\u010dit\u00e9ho {entity_name}", "is_temperature": "Aktu\u00e1ln\u00ed teplota {entity_name}", "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}", "is_voltage": "Aktu\u00e1ln\u00ed nap\u011bt\u00ed {entity_name}" @@ -22,6 +23,7 @@ "gas": "P\u0159i zm\u011bn\u011b mno\u017estv\u00ed plynu {entity_name}", "humidity": "P\u0159i zm\u011bn\u011b vlhkosti {entity_name}", "illuminance": "P\u0159i zm\u011bn\u011b osv\u011btlen\u00ed {entity_name}", + "nitrogen_monoxide": "Zm\u011bna koncentrace oxidu dusnat\u00e9ho {entity_name}", "power": "P\u0159i zm\u011bn\u011b el. v\u00fdkonu {entity_name}", "power_factor": "P\u0159i zm\u011bn\u011b \u00fa\u010din\u00edku {entity_name}", "pressure": "P\u0159i zm\u011bn\u011b tlaku {entity_name}", diff --git a/homeassistant/components/skybell/translations/cs.json b/homeassistant/components/skybell/translations/cs.json new file mode 100644 index 00000000000..08830492748 --- /dev/null +++ b/homeassistant/components/skybell/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/cs.json b/homeassistant/components/solax/translations/cs.json index 7940c6378fe..2bf3fbfa3fb 100644 --- a/homeassistant/components/solax/translations/cs.json +++ b/homeassistant/components/solax/translations/cs.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "ip_address": "IP adresa", "port": "Port" } } diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json index 71b7e84f1e2..ce2c8f4e1a3 100644 --- a/homeassistant/components/soundtouch/translations/id.json +++ b/homeassistant/components/soundtouch/translations/id.json @@ -7,6 +7,11 @@ "cannot_connect": "Gagal terhubung" }, "step": { + "user": { + "data": { + "host": "Host" + } + }, "zeroconf_confirm": { "description": "Anda akan menambahkan perangkat SoundTouch bernama `{name}` ke Home Assistant.", "title": "Konfirmasi penambahan perangkat Bose SoundTouch" diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index b0b94ebe5ab..4a65f5037dd 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Spotify\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "missing_configuration": "Spotify\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_account_mismatch": "\u8a8d\u8a3c\u3055\u308c\u305fSpotify\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u3001\u518d\u8a8d\u8a3c\u304c\u5fc5\u8981\u306a\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, diff --git a/homeassistant/components/steam_online/translations/cs.json b/homeassistant/components/steam_online/translations/cs.json new file mode 100644 index 00000000000..ffd864a8f1d --- /dev/null +++ b/homeassistant/components/steam_online/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/cs.json b/homeassistant/components/syncthing/translations/cs.json index a679dc35fe3..df806ffaf1c 100644 --- a/homeassistant/components/syncthing/translations/cs.json +++ b/homeassistant/components/syncthing/translations/cs.json @@ -4,6 +4,7 @@ "already_configured": "Slu\u017eba je ji\u017e nastavena" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" } } diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json index babf5963357..ae57ebe0d17 100644 --- a/homeassistant/components/ukraine_alarm/translations/ja.json +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -25,7 +25,7 @@ "data": { "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, - "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "\u30e2\u30cb\u30bf\u30fc\u3059\u308b\u72b6\u614b\u3092\u9078\u629e" } } } diff --git a/homeassistant/components/version/translations/cs.json b/homeassistant/components/version/translations/cs.json index 33006d6761b..16fbcb2c8d4 100644 --- a/homeassistant/components/version/translations/cs.json +++ b/homeassistant/components/version/translations/cs.json @@ -2,6 +2,11 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "step": { + "user": { + "title": "Vyberte typ instalace" + } } } } \ No newline at end of file From 36357fe45da4a2a28cf25e59723bba4d57c9167d Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Sat, 9 Jul 2022 14:22:29 +0300 Subject: [PATCH 232/820] Bump pyezviz to 0.2.0.9 (#74755) * Bump ezviz dependency to fix #74618 * Bump ezviz dependency to fix #74618 Co-authored-by: J. Nick Koston --- homeassistant/components/ezviz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index cfdfd6e441d..47e2ec44e8a 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ezviz", "dependencies": ["ffmpeg"], "codeowners": ["@RenierM26", "@baqs"], - "requirements": ["pyezviz==0.2.0.8"], + "requirements": ["pyezviz==0.2.0.9"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["paho_mqtt", "pyezviz"] diff --git a/requirements_all.txt b/requirements_all.txt index 766ceea6e07..d34799e4f6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1492,7 +1492,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.0.8 +pyezviz==0.2.0.9 # homeassistant.components.fido pyfido==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30cbba02ff4..894ca0f2ab4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1001,7 +1001,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.0.8 +pyezviz==0.2.0.9 # homeassistant.components.fido pyfido==2.1.1 From f4c333626eff73aa992db61ce249aa9d094fc359 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Jul 2022 08:29:18 -0500 Subject: [PATCH 233/820] Add coverage for lutron caseta bridges to hkc (#74765) --- .../fixtures/lutron_caseta_bridge.json | 178 ++++++++++++++++++ .../test_lutron_caseta_bridge.py | 55 ++++++ 2 files changed, 233 insertions(+) create mode 100644 tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json create mode 100644 tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py diff --git a/tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json b/tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json new file mode 100644 index 00000000000..40366ad32c6 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json @@ -0,0 +1,178 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 85899345921, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 137438953473, + "perms": ["pr"], + "format": "string", + "value": "Lutron Electronics Co., Inc", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 141733920769, + "perms": ["pr"], + "format": "string", + "value": "L-BDG2-WH", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 150323855361, + "perms": ["pr"], + "format": "string", + "value": "Smart Bridge 2", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 206158430209, + "perms": ["pr"], + "format": "string", + "value": "12344331", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 352187318273, + "perms": ["pr"], + "format": "string", + "value": "08.08", + "description": "Firmware Revision", + "maxLen": 64 + } + ] + }, + { + "iid": 2, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 236223201282, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 21474836482, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 85899345921, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 137438953473, + "perms": ["pr"], + "format": "string", + "value": "Lutron Electronics Co., Inc", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 141733920769, + "perms": ["pr"], + "format": "string", + "value": "PD-FSQN-XX", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 150323855361, + "perms": ["pr"], + "format": "string", + "value": "Cas\u00e9ta\u00ae Wireless Fan Speed Control", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 206158430209, + "perms": ["pr"], + "format": "string", + "value": "39024290", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 352187318273, + "perms": ["pr"], + "format": "string", + "value": "001.005", + "description": "Firmware Revision", + "maxLen": 64 + } + ] + }, + { + "iid": 2, + "type": "00000040-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000029-0000-1000-8000-0026BB765291", + "iid": 176093659138, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 0, + "description": "Rotation Speed", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 25 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 150323855362, + "perms": ["pr"], + "format": "string", + "value": "Cas\u00e9ta\u00ae Wireless Fan Speed Control", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 158913789954, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": false, + "description": "On" + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py new file mode 100644 index 00000000000..8961eb414fa --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py @@ -0,0 +1,55 @@ +"""Tests for handling accessories on a Lutron Caseta bridge via HomeKit.""" + +from homeassistant.const import STATE_OFF + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_lutron_caseta_bridge_setup(hass): + """Test that a Lutron Caseta bridge can be correctly setup in HA via HomeKit.""" + accessories = await setup_accessories_from_file(hass, "lutron_caseta_bridge.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="Smart Bridge 2", + model="L-BDG2-WH", + manufacturer="Lutron Electronics Co., Inc", + sw_version="08.08", + hw_version="", + serial_number="12344331", + devices=[ + DeviceTestInfo( + name="Cas\u00e9ta\u00ae Wireless Fan Speed Control", + model="PD-FSQN-XX", + manufacturer="Lutron Electronics Co., Inc", + sw_version="001.005", + hw_version="", + serial_number="39024290", + unique_id="00:00:00:00:00:00:aid:21474836482", + devices=[], + entities=[ + EntityTestInfo( + entity_id="fan.caseta_r_wireless_fan_speed_control", + friendly_name="Caséta® Wireless Fan Speed Control", + unique_id="homekit-39024290-2", + unit_of_measurement=None, + supported_features=1, + state=STATE_OFF, + capabilities={"preset_modes": None}, + ) + ], + ), + ], + entities=[], + ), + ) From 825e696d2688398f2b9c8c53db1ed3eff0d1d91c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 9 Jul 2022 08:42:32 -0600 Subject: [PATCH 234/820] Migrate Guardian to new entity naming style (#74745) --- homeassistant/components/guardian/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 25e0df913d8..ba4bc5e6ad6 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -373,11 +373,12 @@ class PairedSensorManager: class GuardianEntity(CoordinatorEntity): """Define a base Guardian entity.""" + _attr_has_entity_name = True + def __init__( # pylint: disable=super-init-not-called self, entry: ConfigEntry, description: EntityDescription ) -> None: """Initialize.""" - self._attr_device_info = DeviceInfo(manufacturer="Elexa") self._attr_extra_state_attributes = {} self._entry = entry self.entity_description = description @@ -406,12 +407,11 @@ class PairedSensorEntity(GuardianEntity): paired_sensor_uid = coordinator.data["uid"] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, paired_sensor_uid)}, + manufacturer="Elexa", + model=coordinator.data["codename"], name=f"Guardian Paired Sensor {paired_sensor_uid}", via_device=(DOMAIN, entry.data[CONF_UID]), ) - self._attr_name = ( - f"Guardian Paired Sensor {paired_sensor_uid}: {description.name}" - ) self._attr_unique_id = f"{paired_sensor_uid}_{description.key}" self.coordinator = coordinator @@ -434,10 +434,10 @@ class ValveControllerEntity(GuardianEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.data[CONF_UID])}, + manufacturer="Elexa", model=coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], name=f"Guardian Valve Controller {entry.data[CONF_UID]}", ) - self._attr_name = f"Guardian {entry.data[CONF_UID]}: {description.name}" self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" self.coordinators = coordinators From e17db1fd0c8935e61e6d871789e4e24810d6802d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 9 Jul 2022 10:43:50 -0400 Subject: [PATCH 235/820] Remove deprecated yaml config from Steam (#74805) --- .../components/steam_online/config_flow.py | 53 +++---------------- .../components/steam_online/sensor.py | 36 ++----------- tests/components/steam_online/__init__.py | 9 ---- .../steam_online/test_config_flow.py | 32 +---------- 4 files changed, 12 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 7ed1f0a3610..8356ad8bbc6 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -12,29 +12,16 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_ACCOUNT, - CONF_ACCOUNTS, - DEFAULT_NAME, - DOMAIN, - LOGGER, - PLACEHOLDERS, -) +from .const import CONF_ACCOUNT, CONF_ACCOUNTS, DOMAIN, LOGGER, PLACEHOLDERS -def validate_input( - user_input: dict[str, str | int], multi: bool = False -) -> list[dict[str, str | int]]: +def validate_input(user_input: dict[str, str]) -> dict[str, str | int]: """Handle common flow input validation.""" steam.api.key.set(user_input[CONF_API_KEY]) interface = steam.api.interface("ISteamUser") - if multi: - names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNTS]) - else: - names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNT]) - return names["response"]["players"]["player"] + names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNT]) + return names["response"]["players"]["player"][0] class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -62,8 +49,8 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): elif user_input is not None: try: res = await self.hass.async_add_executor_job(validate_input, user_input) - if res[0] is not None: - name = str(res[0]["personaname"]) + if res is not None: + name = str(res["personaname"]) else: errors["base"] = "invalid_account" except (steam.api.HTTPError, steam.api.HTTPTimeoutError) as ex: @@ -80,22 +67,10 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="reauth_successful") self._abort_if_unique_id_configured() - if self.source == config_entries.SOURCE_IMPORT: - res = await self.hass.async_add_executor_job( - validate_input, user_input, True - ) - accounts_data = { - CONF_ACCOUNTS: { - acc["steamid"]: acc["personaname"] for acc in res - } - } - user_input.pop(CONF_ACCOUNTS) - else: - accounts_data = {CONF_ACCOUNTS: {user_input[CONF_ACCOUNT]: name}} return self.async_create_entry( - title=name or DEFAULT_NAME, + title=name, data=user_input, - options=accounts_data, + options={CONF_ACCOUNTS: {user_input[CONF_ACCOUNT]: name}}, ) user_input = user_input or {} return self.async_show_form( @@ -114,18 +89,6 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=PLACEHOLDERS, ) - async def async_step_import(self, import_config: ConfigType) -> FlowResult: - """Import a config entry from configuration.yaml.""" - for entry in self._async_current_entries(): - if entry.data[CONF_API_KEY] == import_config[CONF_API_KEY]: - return self.async_abort(reason="already_configured") - LOGGER.warning( - "Steam yaml config is now deprecated and has been imported. " - "Please remove it from your config" - ) - import_config[CONF_ACCOUNT] = import_config[CONF_ACCOUNTS][0] - return await self.async_step_user(import_config) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index be175b41b66..307dfac0542 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -4,19 +4,11 @@ from __future__ import annotations from datetime import datetime from time import localtime, mktime -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utc_from_timestamp from . import SteamEntity @@ -31,31 +23,9 @@ from .const import ( ) from .coordinator import SteamDataUpdateCoordinator -# Deprecated in Home Assistant 2022.5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_ACCOUNTS, default=[]): vol.All(cv.ensure_list, [cv.string]), - } -) - PARALLEL_UPDATES = 1 -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Twitch sensor from yaml.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/tests/components/steam_online/__init__.py b/tests/components/steam_online/__init__.py index 4c8c398502f..d41554e4d04 100644 --- a/tests/components/steam_online/__init__.py +++ b/tests/components/steam_online/__init__.py @@ -30,15 +30,6 @@ CONF_OPTIONS_2 = { } } -CONF_IMPORT_OPTIONS = { - CONF_ACCOUNTS: { - ACCOUNT_1: ACCOUNT_NAME_1, - ACCOUNT_2: ACCOUNT_NAME_2, - } -} - -CONF_IMPORT_DATA = {CONF_API_KEY: API_KEY, CONF_ACCOUNTS: [ACCOUNT_1, ACCOUNT_2]} - def create_entry(hass: HomeAssistant) -> MockConfigEntry: """Add config entry in Home Assistant.""" diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index 51ecd77c508..1844611530d 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -5,7 +5,7 @@ import steam from homeassistant import data_entry_flow from homeassistant.components.steam_online.const import CONF_ACCOUNTS, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -15,8 +15,6 @@ from . import ( ACCOUNT_2, ACCOUNT_NAME_1, CONF_DATA, - CONF_IMPORT_DATA, - CONF_IMPORT_OPTIONS, CONF_OPTIONS, CONF_OPTIONS_2, create_entry, @@ -137,34 +135,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: assert entry.data == new_conf -async def test_flow_import(hass: HomeAssistant) -> None: - """Test import step.""" - with patch_interface(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == ACCOUNT_NAME_1 - assert result["data"] == CONF_DATA - assert result["options"] == CONF_IMPORT_OPTIONS - assert result["result"].unique_id == ACCOUNT_1 - - -async def test_flow_import_already_configured(hass: HomeAssistant) -> None: - """Test import step already configured.""" - create_entry(hass) - with patch_interface(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" - - async def test_options_flow(hass: HomeAssistant) -> None: """Test updating options.""" entry = create_entry(hass) From 79b34090e88d3610da7d93173b2232d682144494 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 9 Jul 2022 10:54:15 -0400 Subject: [PATCH 236/820] Bump aiopyarr to 22.7.0 (#74749) --- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 6b34b077888..6b9b45b75ec 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.6.0"], + "requirements": ["aiopyarr==22.7.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index d34799e4f6f..8f0fcb9a608 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -226,7 +226,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.6.0 +aiopyarr==22.7.0 # homeassistant.components.qnap_qsw aioqsw==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 894ca0f2ab4..6d792218e83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -195,7 +195,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.6.0 +aiopyarr==22.7.0 # homeassistant.components.qnap_qsw aioqsw==0.1.0 From 430d3e4604e6929e6ef671282b267a4fc8622b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 9 Jul 2022 18:07:46 +0300 Subject: [PATCH 237/820] Look for huawei_lte device MACs in a few more device info attributes (#74795) --- homeassistant/components/huawei_lte/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/utils.py b/homeassistant/components/huawei_lte/utils.py index 37f9f1a5542..bbcf29e552c 100644 --- a/homeassistant/components/huawei_lte/utils.py +++ b/homeassistant/components/huawei_lte/utils.py @@ -14,7 +14,10 @@ def get_device_macs( :param device_info: the device.information structure for the device :param wlan_settings: the wlan.multi_basic_settings structure for the device """ - macs = [device_info.get("MacAddress1"), device_info.get("MacAddress2")] + macs = [ + device_info.get(x) + for x in ("MacAddress1", "MacAddress2", "WifiMacAddrWl0", "WifiMacAddrWl1") + ] try: macs.extend(x.get("WifiMac") for x in wlan_settings["Ssids"]["Ssid"]) except Exception: # pylint: disable=broad-except From a9c97e5d3a10763132ca045ba32da4265adbcfe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 9 Jul 2022 18:08:40 +0300 Subject: [PATCH 238/820] Sort huawei_lte sensor meta dict, add section separators (#74782) --- homeassistant/components/huawei_lte/sensor.py | 217 ++++++++++-------- 1 file changed, 116 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 85f63ed2bc6..642d8368207 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -62,9 +62,18 @@ class SensorMeta(NamedTuple): SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { + # + # Device information + # KEY_DEVICE_INFORMATION: SensorMeta( include=re.compile(r"^(WanIP.*Address|uptime)$", re.IGNORECASE) ), + (KEY_DEVICE_INFORMATION, "uptime"): SensorMeta( + name="Uptime", + icon="mdi:timer-outline", + native_unit_of_measurement=TIME_SECONDS, + entity_category=EntityCategory.DIAGNOSTIC, + ), (KEY_DEVICE_INFORMATION, "WanIPAddress"): SensorMeta( name="WAN IP address", icon="mdi:ip", @@ -76,12 +85,9 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_INFORMATION, "uptime"): SensorMeta( - name="Uptime", - icon="mdi:timer-outline", - native_unit_of_measurement=TIME_SECONDS, - entity_category=EntityCategory.DIAGNOSTIC, - ), + # + # Signal + # (KEY_DEVICE_SIGNAL, "band"): SensorMeta( name="Band", entity_category=EntityCategory.DIAGNOSTIC, @@ -91,6 +97,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), + (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( + name="CQI 0", + icon="mdi:speedometer", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( + name="CQI 1", + icon="mdi:speedometer", + ), (KEY_DEVICE_SIGNAL, "dl_mcs"): SensorMeta( name="Downlink MCS", entity_category=EntityCategory.DIAGNOSTIC, @@ -108,49 +123,42 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="EARFCN", entity_category=EntityCategory.DIAGNOSTIC, ), + (KEY_DEVICE_SIGNAL, "ecio"): SensorMeta( + name="EC/IO", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + # https://wiki.teltonika.lt/view/EC/IO + icon=lambda x: ( + "mdi:signal-cellular-outline", + "mdi:signal-cellular-1", + "mdi:signal-cellular-2", + "mdi:signal-cellular-3", + )[bisect((-20, -10, -6), x if x is not None else -1000)], + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "enodeb_id"): SensorMeta( + name="eNodeB ID", + entity_category=EntityCategory.DIAGNOSTIC, + ), (KEY_DEVICE_SIGNAL, "lac"): SensorMeta( name="LAC", icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta( - name="PLMN", + (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( + name="Downlink frequency", + formatter=lambda x: ( + round(int(x) / 10) if x is not None else None, + FREQUENCY_MEGAHERTZ, + ), entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "rac"): SensorMeta( - name="RAC", - icon="mdi:map-marker", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta( - name="RRC status", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "tac"): SensorMeta( - name="TAC", - icon="mdi:map-marker", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta( - name="TDD", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "txpower"): SensorMeta( - name="Transmit power", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta( - name="Uplink MCS", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "ulbandwidth"): SensorMeta( - name="Uplink bandwidth", - icon=lambda x: ( - "mdi:speedometer-slow", - "mdi:speedometer-medium", - "mdi:speedometer", - )[bisect((8, 15), x if x is not None else -1000)], + (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( + name="Uplink frequency", + formatter=lambda x: ( + round(int(x) / 10) if x is not None else None, + FREQUENCY_MEGAHERTZ, + ), entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "mode"): SensorMeta( @@ -168,19 +176,31 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( - name="RSRQ", + (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta( + name="PLMN", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rac"): SensorMeta( + name="RAC", + icon="mdi:map-marker", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta( + name="RRC status", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta( + name="RSCP", device_class=SensorDeviceClass.SIGNAL_STRENGTH, - # http://www.lte-anbieter.info/technik/rsrq.php + # https://wiki.teltonika.lt/view/RSCP icon=lambda x: ( "mdi:signal-cellular-outline", "mdi:signal-cellular-1", "mdi:signal-cellular-2", "mdi:signal-cellular-3", - )[bisect((-11, -8, -5), x if x is not None else -1000)], + )[bisect((-95, -85, -75), x if x is not None else -1000)], state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rsrp"): SensorMeta( name="RSRP", @@ -196,6 +216,20 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), + (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( + name="RSRQ", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + # http://www.lte-anbieter.info/technik/rsrq.php + icon=lambda x: ( + "mdi:signal-cellular-outline", + "mdi:signal-cellular-1", + "mdi:signal-cellular-2", + "mdi:signal-cellular-3", + )[bisect((-11, -8, -5), x if x is not None else -1000)], + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + ), (KEY_DEVICE_SIGNAL, "rssi"): SensorMeta( name="RSSI", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -224,65 +258,40 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), - (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta( - name="RSCP", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - # https://wiki.teltonika.lt/view/RSCP - icon=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-95, -85, -75), x if x is not None else -1000)], - state_class=SensorStateClass.MEASUREMENT, + (KEY_DEVICE_SIGNAL, "tac"): SensorMeta( + name="TAC", + icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "ecio"): SensorMeta( - name="EC/IO", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - # https://wiki.teltonika.lt/view/EC/IO - icon=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-20, -10, -6), x if x is not None else -1000)], - state_class=SensorStateClass.MEASUREMENT, + (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta( + name="TDD", entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "transmode"): SensorMeta( name="Transmission mode", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( - name="CQI 0", - icon="mdi:speedometer", + (KEY_DEVICE_SIGNAL, "txpower"): SensorMeta( + name="Transmit power", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( - name="CQI 1", - icon="mdi:speedometer", - ), - (KEY_DEVICE_SIGNAL, "enodeb_id"): SensorMeta( - name="eNodeB ID", + (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta( + name="Uplink MCS", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( - name="Downlink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - FREQUENCY_MEGAHERTZ, - ), - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( - name="Uplink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - FREQUENCY_MEGAHERTZ, - ), + (KEY_DEVICE_SIGNAL, "ulbandwidth"): SensorMeta( + name="Uplink bandwidth", + icon=lambda x: ( + "mdi:speedometer-slow", + "mdi:speedometer-medium", + "mdi:speedometer", + )[bisect((8, 15), x if x is not None else -1000)], entity_category=EntityCategory.DIAGNOSTIC, ), + # + # Monitoring + # KEY_MONITORING_CHECK_NOTIFICATIONS: SensorMeta( exclude=re.compile( r"^(onlineupdatestatus|smsstoragefull)$", @@ -331,13 +340,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_MONITORING_STATUS, "SecondaryDns"): SensorMeta( - name="Secondary DNS server", + (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): SensorMeta( + name="Primary IPv6 DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): SensorMeta( - name="Primary IPv6 DNS server", + (KEY_MONITORING_STATUS, "SecondaryDns"): SensorMeta( + name="Secondary DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -396,14 +405,12 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), + # + # Network + # KEY_NET_CURRENT_PLMN: SensorMeta( exclude=re.compile(r"^(Rat|ShortName|Spn)$", re.IGNORECASE) ), - (KEY_NET_CURRENT_PLMN, "State"): SensorMeta( - name="Operator search mode", - formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), - entity_category=EntityCategory.CONFIG, - ), (KEY_NET_CURRENT_PLMN, "FullName"): SensorMeta( name="Operator name", entity_category=EntityCategory.DIAGNOSTIC, @@ -412,6 +419,11 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="Operator code", entity_category=EntityCategory.DIAGNOSTIC, ), + (KEY_NET_CURRENT_PLMN, "State"): SensorMeta( + name="Operator search mode", + formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), + entity_category=EntityCategory.CONFIG, + ), KEY_NET_NET_MODE: SensorMeta(include=re.compile(r"^NetworkMode$", re.IGNORECASE)), (KEY_NET_NET_MODE, "NetworkMode"): SensorMeta( name="Preferred mode", @@ -429,6 +441,9 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), entity_category=EntityCategory.CONFIG, ), + # + # SMS + # (KEY_SMS_SMS_COUNT, "LocalDeleted"): SensorMeta( name="SMS deleted (device)", icon="mdi:email-minus", From 8bf692d046373238601b0012712de7cd3e6a63df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 9 Jul 2022 17:13:27 +0200 Subject: [PATCH 239/820] Update aioqsw to v0.1.1 (#74784) --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index be565f2a07e..83c9423f0f4 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -3,7 +3,7 @@ "name": "QNAP QSW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", - "requirements": ["aioqsw==0.1.0"], + "requirements": ["aioqsw==0.1.1"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioqsw"], diff --git a/requirements_all.txt b/requirements_all.txt index 8f0fcb9a608..3f96f8eb711 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -229,7 +229,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.0 +aioqsw==0.1.1 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d792218e83..ef1b8f5941e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,7 +198,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.0 +aioqsw==0.1.1 # homeassistant.components.recollect_waste aiorecollect==1.0.8 From 3a5cca3ff2c9e649eac1d81842160f56e78eee15 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 9 Jul 2022 17:15:08 +0200 Subject: [PATCH 240/820] Convert rfxtrx to entity naming (#74720) --- homeassistant/components/rfxtrx/__init__.py | 19 ++--------- homeassistant/components/rfxtrx/sensor.py | 28 +++++++++++++++- tests/components/rfxtrx/test_config_flow.py | 24 +++++++------- tests/components/rfxtrx/test_sensor.py | 36 ++++++++++----------- 4 files changed, 60 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index b517abafc86..60ca25ed6d7 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -456,6 +456,9 @@ class RfxtrxEntity(RestoreEntity): Contains the common logic for Rfxtrx lights and switches. """ + _attr_assumed_state = True + _attr_has_entity_name = True + _attr_should_poll = False _device: rfxtrxmod.RFXtrxDevice _event: rfxtrxmod.RFXtrxEvent | None @@ -466,7 +469,6 @@ class RfxtrxEntity(RestoreEntity): event: rfxtrxmod.RFXtrxEvent | None = None, ) -> None: """Initialize the device.""" - self._name = f"{device.type_string} {device.id_string}" self._device = device self._event = event self._device_id = device_id @@ -484,16 +486,6 @@ class RfxtrxEntity(RestoreEntity): async_dispatcher_connect(self.hass, SIGNAL_EVENT, self._handle_event) ) - @property - def should_poll(self): - """No polling needed for a RFXtrx switch.""" - return False - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - @property def extra_state_attributes(self): """Return the device state attributes.""" @@ -501,11 +493,6 @@ class RfxtrxEntity(RestoreEntity): return None return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - @property - def assumed_state(self): - """Return true if unable to access real state of entity.""" - return True - @property def unique_id(self): """Return unique identifier of remote device.""" diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 3111f0eabd7..6ff3073b900 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -63,12 +63,14 @@ class RfxtrxSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES = ( RfxtrxSensorEntityDescription( key="Barometer", + name="Barometer", device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_HPA, ), RfxtrxSensorEntityDescription( key="Battery numeric", + name="Battery", device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -77,42 +79,49 @@ SENSOR_TYPES = ( ), RfxtrxSensorEntityDescription( key="Current", + name="Current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 1", + name="Current Ch. 1", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 2", + name="Current Ch. 2", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 3", + name="Current Ch. 3", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Energy usage", + name="Instantaneous power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), RfxtrxSensorEntityDescription( key="Humidity", + name="Humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), RfxtrxSensorEntityDescription( key="Rssi numeric", + name="Signal strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -121,86 +130,104 @@ SENSOR_TYPES = ( ), RfxtrxSensorEntityDescription( key="Temperature", + name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Temperature2", + name="Temperature 2", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Total usage", + name="Total energy usage", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), RfxtrxSensorEntityDescription( key="Voltage", + name="Voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, ), RfxtrxSensorEntityDescription( key="Wind direction", + name="Wind direction", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DEGREE, ), RfxtrxSensorEntityDescription( key="Rain rate", + name="Rain rate", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), RfxtrxSensorEntityDescription( key="Sound", + name="Sound", ), RfxtrxSensorEntityDescription( key="Sensor Status", + name="Sensor status", ), RfxtrxSensorEntityDescription( key="Count", + name="Count", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Counter value", + name="Counter value", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Chill", + name="Chill", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Wind average speed", + name="Wind average speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Wind gust", + name="Wind gust", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Rain total", + name="Rain total", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=LENGTH_MILLIMETERS, ), RfxtrxSensorEntityDescription( key="Forecast", + name="Forecast status", ), RfxtrxSensorEntityDescription( key="Forecast numeric", + name="Forecast", ), RfxtrxSensorEntityDescription( key="Humidity status", + name="Humidity status", ), RfxtrxSensorEntityDescription( key="UV", + name="UV index", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UV_INDEX, ), @@ -252,7 +279,6 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Initialize the sensor.""" super().__init__(device, device_id, event=event) self.entity_description = entity_description - self._name = f"{device.type_string} {device.id_string} {entity_description.key}" self._unique_id = "_".join( x for x in (*self._device_id, entity_description.key) ) diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 23ce5d1393f..555d780fe92 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -475,11 +475,11 @@ async def test_options_replace_sensor_device(hass): await start_options_flow(hass, entry) state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_signal_strength" ) assert state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery" ) assert state state = hass.states.get( @@ -495,11 +495,11 @@ async def test_options_replace_sensor_device(hass): ) assert state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_signal_strength" ) assert state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery" ) assert state state = hass.states.get( @@ -565,7 +565,7 @@ async def test_options_replace_sensor_device(hass): entity_registry = er.async_get(hass) entry = entity_registry.async_get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_signal_strength" ) assert entry assert entry.device_id == new_device @@ -580,7 +580,7 @@ async def test_options_replace_sensor_device(hass): assert entry assert entry.device_id == new_device entry = entity_registry.async_get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery" ) assert entry assert entry.device_id == new_device @@ -591,11 +591,11 @@ async def test_options_replace_sensor_device(hass): assert entry.device_id == new_device state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_signal_strength" ) assert not state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery" ) assert not state state = hass.states.get( @@ -637,13 +637,13 @@ async def test_options_replace_control_device(hass): state = hass.states.get("binary_sensor.ac_118cdea_2") assert state - state = hass.states.get("sensor.ac_118cdea_2_rssi_numeric") + state = hass.states.get("sensor.ac_118cdea_2_signal_strength") assert state state = hass.states.get("switch.ac_118cdea_2") assert state state = hass.states.get("binary_sensor.ac_1118cdea_2") assert state - state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric") + state = hass.states.get("sensor.ac_1118cdea_2_signal_strength") assert state state = hass.states.get("switch.ac_1118cdea_2") assert state @@ -700,7 +700,7 @@ async def test_options_replace_control_device(hass): entry = entity_registry.async_get("binary_sensor.ac_118cdea_2") assert entry assert entry.device_id == new_device - entry = entity_registry.async_get("sensor.ac_118cdea_2_rssi_numeric") + entry = entity_registry.async_get("sensor.ac_118cdea_2_signal_strength") assert entry assert entry.device_id == new_device entry = entity_registry.async_get("switch.ac_118cdea_2") @@ -709,7 +709,7 @@ async def test_options_replace_control_device(hass): state = hass.states.get("binary_sensor.ac_1118cdea_2") assert not state - state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric") + state = hass.states.get("sensor.ac_1118cdea_2_signal_strength") assert not state state = hass.states.get("switch.ac_1118cdea_2") assert not state diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 77f9960de49..8ba0104cf50 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -101,19 +101,19 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): assert state.attributes.get("friendly_name") == f"{base_name} Humidity status" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == f"{base_name} Rssi numeric" + assert state.attributes.get("friendly_name") == f"{base_name} Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT ) - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == f"{base_name} Battery numeric" + assert state.attributes.get("friendly_name") == f"{base_name} Battery" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -179,7 +179,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "normal" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "-64" assert ( @@ -192,7 +192,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "18.4" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -211,7 +211,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "normal" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "-64" assert ( @@ -224,7 +224,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "14.9" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -243,7 +243,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "normal" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "-64" assert ( @@ -256,7 +256,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "17.9" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -328,19 +328,19 @@ async def test_rssi_sensor(hass, rfxtrx): await hass.async_block_till_done() await hass.async_start() - state = hass.states.get("sensor.pt2262_22670e_rssi_numeric") + state = hass.states.get("sensor.pt2262_22670e_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == "PT2262 22670e Rssi numeric" + assert state.attributes.get("friendly_name") == "PT2262 22670e Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT ) - state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") + state = hass.states.get("sensor.ac_213c7f2_48_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == "AC 213c7f2:48 Rssi numeric" + assert state.attributes.get("friendly_name") == "AC 213c7f2:48 Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT @@ -349,21 +349,21 @@ async def test_rssi_sensor(hass, rfxtrx): await rfxtrx.signal("0913000022670e013b70") await rfxtrx.signal("0b1100cd0213c7f230010f71") - state = hass.states.get("sensor.pt2262_22670e_rssi_numeric") + state = hass.states.get("sensor.pt2262_22670e_signal_strength") assert state assert state.state == "-64" - state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") + state = hass.states.get("sensor.ac_213c7f2_48_signal_strength") assert state assert state.state == "-64" await rfxtrx.signal("0913000022670e013b60") await rfxtrx.signal("0b1100cd0213c7f230010f61") - state = hass.states.get("sensor.pt2262_22670e_rssi_numeric") + state = hass.states.get("sensor.pt2262_22670e_signal_strength") assert state assert state.state == "-72" - state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") + state = hass.states.get("sensor.ac_213c7f2_48_signal_strength") assert state assert state.state == "-72" From cd03c49fc27d3b2c8c33ad032a9207acdd71b9ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Jul 2022 10:27:42 -0500 Subject: [PATCH 241/820] Wait for config entry platform forwards (#73806) --- homeassistant/components/abode/__init__.py | 2 +- .../components/accuweather/__init__.py | 2 +- homeassistant/components/acmeda/__init__.py | 2 +- homeassistant/components/adax/__init__.py | 2 +- homeassistant/components/adguard/__init__.py | 2 +- .../components/advantage_air/__init__.py | 2 +- homeassistant/components/aemet/__init__.py | 2 +- .../components/agent_dvr/__init__.py | 2 +- homeassistant/components/airly/__init__.py | 2 +- homeassistant/components/airnow/__init__.py | 2 +- .../components/airthings/__init__.py | 2 +- .../components/airtouch4/__init__.py | 2 +- .../components/airvisual/__init__.py | 2 +- homeassistant/components/airzone/__init__.py | 2 +- .../components/aladdin_connect/__init__.py | 2 +- .../components/alarmdecoder/__init__.py | 2 +- homeassistant/components/ambee/__init__.py | 2 +- .../components/amberelectric/__init__.py | 2 +- .../components/ambiclimate/__init__.py | 2 +- .../components/androidtv/__init__.py | 2 +- homeassistant/components/apple_tv/__init__.py | 13 ++----- .../components/arcam_fmj/__init__.py | 2 +- .../components/aseko_pool_live/__init__.py | 2 +- homeassistant/components/asuswrt/__init__.py | 2 +- homeassistant/components/atag/__init__.py | 2 +- homeassistant/components/august/__init__.py | 2 +- homeassistant/components/aurora/__init__.py | 2 +- .../aurora_abb_powerone/__init__.py | 2 +- .../components/aussie_broadband/__init__.py | 2 +- homeassistant/components/awair/__init__.py | 2 +- .../components/azure_devops/__init__.py | 2 +- homeassistant/components/baf/__init__.py | 2 +- homeassistant/components/balboa/__init__.py | 2 +- homeassistant/components/blebox/__init__.py | 3 +- homeassistant/components/blink/__init__.py | 2 +- .../bmw_connected_drive/__init__.py | 2 +- homeassistant/components/bond/__init__.py | 2 +- .../components/bosch_shc/__init__.py | 2 +- homeassistant/components/braviatv/__init__.py | 2 +- homeassistant/components/broadlink/device.py | 2 +- homeassistant/components/brother/__init__.py | 2 +- homeassistant/components/brunt/__init__.py | 2 +- homeassistant/components/bsblan/__init__.py | 2 +- .../components/buienradar/__init__.py | 2 +- homeassistant/components/canary/__init__.py | 2 +- .../components/cert_expiry/__init__.py | 2 +- .../components/climacell/__init__.py | 2 +- .../components/co2signal/__init__.py | 2 +- homeassistant/components/coinbase/__init__.py | 2 +- homeassistant/components/control4/__init__.py | 2 +- .../components/coolmaster/__init__.py | 2 +- .../components/coronavirus/__init__.py | 2 +- homeassistant/components/cpuspeed/__init__.py | 2 +- .../components/crownstone/entry_manager.py | 4 ++- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/deluge/__init__.py | 2 +- homeassistant/components/demo/__init__.py | 7 ++-- homeassistant/components/denonavr/__init__.py | 2 +- .../components/derivative/__init__.py | 2 +- .../components/device_tracker/__init__.py | 8 +++++ .../devolo_home_control/__init__.py | 2 +- .../devolo_home_network/__init__.py | 2 +- homeassistant/components/dexcom/__init__.py | 2 +- homeassistant/components/directv/__init__.py | 2 +- homeassistant/components/dlna_dmr/__init__.py | 2 +- homeassistant/components/dnsip/__init__.py | 2 +- homeassistant/components/doorbird/__init__.py | 2 +- homeassistant/components/dsmr/__init__.py | 2 +- homeassistant/components/dunehd/__init__.py | 2 +- homeassistant/components/dynalite/__init__.py | 2 +- homeassistant/components/eafm/__init__.py | 2 +- homeassistant/components/ecobee/__init__.py | 2 +- homeassistant/components/econet/__init__.py | 2 +- homeassistant/components/efergy/__init__.py | 2 +- .../components/eight_sleep/__init__.py | 2 +- homeassistant/components/elgato/__init__.py | 2 +- homeassistant/components/elkm1/__init__.py | 2 +- homeassistant/components/elmax/__init__.py | 2 +- homeassistant/components/emonitor/__init__.py | 2 +- .../components/enphase_envoy/__init__.py | 2 +- .../components/environment_canada/__init__.py | 2 +- homeassistant/components/epson/__init__.py | 2 +- homeassistant/components/esphome/__init__.py | 19 +++++------ .../components/esphome/entry_data.py | 9 ++--- .../components/evil_genius_labs/__init__.py | 2 +- homeassistant/components/ezviz/__init__.py | 2 +- .../components/faa_delays/__init__.py | 2 +- homeassistant/components/fibaro/__init__.py | 2 +- homeassistant/components/filesize/__init__.py | 2 +- .../components/fireservicerota/__init__.py | 2 +- homeassistant/components/firmata/__init__.py | 13 ++++--- homeassistant/components/fivem/__init__.py | 2 +- .../components/flick_electric/__init__.py | 2 +- homeassistant/components/flipr/__init__.py | 2 +- homeassistant/components/flo/__init__.py | 2 +- homeassistant/components/flume/__init__.py | 2 +- .../components/flunearyou/__init__.py | 2 +- homeassistant/components/flux_led/__init__.py | 2 +- .../components/forecast_solar/__init__.py | 2 +- .../components/forked_daapd/__init__.py | 2 +- homeassistant/components/foscam/__init__.py | 2 +- homeassistant/components/freebox/__init__.py | 2 +- .../components/freedompro/__init__.py | 2 +- homeassistant/components/fritz/__init__.py | 2 +- homeassistant/components/fritzbox/__init__.py | 2 +- .../fritzbox_callmonitor/__init__.py | 2 +- homeassistant/components/fronius/__init__.py | 2 +- .../components/garages_amsterdam/__init__.py | 2 +- homeassistant/components/gdacs/__init__.py | 4 ++- homeassistant/components/generic/__init__.py | 2 +- .../components/geocaching/__init__.py | 2 +- homeassistant/components/geofency/__init__.py | 2 +- .../components/geonetnz_quakes/__init__.py | 4 ++- .../components/geonetnz_volcano/__init__.py | 4 ++- homeassistant/components/gios/__init__.py | 2 +- homeassistant/components/github/__init__.py | 2 +- homeassistant/components/glances/__init__.py | 4 ++- homeassistant/components/goalzero/__init__.py | 2 +- .../components/gogogate2/__init__.py | 2 +- homeassistant/components/goodwe/__init__.py | 2 +- homeassistant/components/google/__init__.py | 2 +- .../components/google_travel_time/__init__.py | 2 +- .../components/gpslogger/__init__.py | 2 +- homeassistant/components/gree/__init__.py | 2 +- homeassistant/components/group/__init__.py | 4 ++- .../components/growatt_server/__init__.py | 2 +- homeassistant/components/guardian/__init__.py | 2 +- homeassistant/components/habitica/__init__.py | 2 +- homeassistant/components/harmony/__init__.py | 2 +- homeassistant/components/hassio/__init__.py | 2 +- .../components/here_travel_time/__init__.py | 2 +- .../components/hisense_aehw4a1/__init__.py | 2 +- homeassistant/components/hive/__init__.py | 14 ++++---- homeassistant/components/hlk_sw16/__init__.py | 34 ++++++++----------- .../components/home_connect/__init__.py | 2 +- .../components/home_plus_control/__init__.py | 15 ++------ .../components/homewizard/__init__.py | 2 +- .../components/honeywell/__init__.py | 2 +- .../components/huawei_lte/__init__.py | 2 +- homeassistant/components/hue/bridge.py | 4 +-- .../components/huisbaasje/__init__.py | 2 +- .../hunterdouglas_powerview/__init__.py | 2 +- .../components/hvv_departures/__init__.py | 2 +- homeassistant/components/hyperion/__init__.py | 21 ++++-------- homeassistant/components/ialarm/__init__.py | 2 +- .../components/iaqualink/__init__.py | 14 ++++---- homeassistant/components/icloud/__init__.py | 2 +- homeassistant/components/insteon/__init__.py | 5 +-- .../components/integration/__init__.py | 2 +- .../components/intellifire/__init__.py | 2 +- homeassistant/components/ios/__init__.py | 2 +- homeassistant/components/iotawatt/__init__.py | 2 +- homeassistant/components/ipma/__init__.py | 2 +- homeassistant/components/ipp/__init__.py | 2 +- homeassistant/components/iqvia/__init__.py | 2 +- homeassistant/components/iss/__init__.py | 2 +- homeassistant/components/isy994/__init__.py | 2 +- homeassistant/components/izone/__init__.py | 2 +- homeassistant/components/juicenet/__init__.py | 2 +- .../components/kaleidescape/__init__.py | 2 +- .../components/keenetic_ndms2/__init__.py | 2 +- homeassistant/components/kmtronic/__init__.py | 2 +- homeassistant/components/kodi/__init__.py | 2 +- .../components/kostal_plenticore/__init__.py | 2 +- homeassistant/components/kraken/__init__.py | 2 +- homeassistant/components/kulersky/__init__.py | 2 +- .../components/launch_library/__init__.py | 2 +- .../components/laundrify/__init__.py | 2 +- homeassistant/components/lcn/__init__.py | 2 +- homeassistant/components/lifx/__init__.py | 2 +- homeassistant/components/litejet/__init__.py | 2 +- .../components/litterrobot/__init__.py | 2 +- homeassistant/components/local_ip/__init__.py | 2 +- homeassistant/components/locative/__init__.py | 2 +- .../components/logi_circle/__init__.py | 2 +- homeassistant/components/lookin/__init__.py | 2 +- .../components/luftdaten/__init__.py | 2 +- .../components/lutron_caseta/__init__.py | 2 +- homeassistant/components/lyric/__init__.py | 2 +- homeassistant/components/mazda/__init__.py | 2 +- homeassistant/components/meater/__init__.py | 2 +- homeassistant/components/melcloud/__init__.py | 2 +- homeassistant/components/met/__init__.py | 2 +- .../components/met_eireann/__init__.py | 2 +- .../components/meteo_france/__init__.py | 2 +- .../components/meteoclimatic/__init__.py | 2 +- .../components/metoffice/__init__.py | 2 +- homeassistant/components/mill/__init__.py | 2 +- homeassistant/components/min_max/__init__.py | 2 +- .../components/minecraft_server/__init__.py | 2 +- homeassistant/components/mjpeg/__init__.py | 2 +- .../components/mobile_app/__init__.py | 2 +- .../components/modem_callerid/__init__.py | 2 +- .../components/modern_forms/__init__.py | 2 +- .../components/moehlenhoff_alpha2/__init__.py | 2 +- .../components/monoprice/__init__.py | 2 +- homeassistant/components/moon/__init__.py | 2 +- .../components/motion_blinds/__init__.py | 2 +- .../components/motioneye/__init__.py | 21 ++++-------- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mullvad/__init__.py | 2 +- homeassistant/components/mutesync/__init__.py | 2 +- homeassistant/components/myq/__init__.py | 2 +- .../components/mysensors/__init__.py | 15 +++----- homeassistant/components/nam/__init__.py | 2 +- homeassistant/components/nanoleaf/__init__.py | 2 +- homeassistant/components/neato/__init__.py | 2 +- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/netatmo/__init__.py | 2 +- homeassistant/components/netgear/__init__.py | 2 +- homeassistant/components/nexia/__init__.py | 2 +- .../components/nightscout/__init__.py | 2 +- homeassistant/components/nina/__init__.py | 2 +- .../components/nmap_tracker/__init__.py | 2 +- homeassistant/components/notify/__init__.py | 7 ++++ homeassistant/components/notion/__init__.py | 2 +- homeassistant/components/nuheat/__init__.py | 2 +- homeassistant/components/nuki/__init__.py | 2 +- homeassistant/components/nut/__init__.py | 2 +- homeassistant/components/nws/__init__.py | 2 +- homeassistant/components/nzbget/__init__.py | 2 +- .../components/octoprint/__init__.py | 2 +- .../components/omnilogic/__init__.py | 2 +- homeassistant/components/oncue/__init__.py | 2 +- .../components/ondilo_ico/__init__.py | 2 +- homeassistant/components/onewire/__init__.py | 2 +- homeassistant/components/onvif/__init__.py | 2 +- .../components/open_meteo/__init__.py | 2 +- .../components/opengarage/__init__.py | 2 +- .../components/opentherm_gw/__init__.py | 2 +- homeassistant/components/openuv/__init__.py | 2 +- .../components/openweathermap/__init__.py | 2 +- homeassistant/components/overkiz/__init__.py | 2 +- .../components/ovo_energy/__init__.py | 2 +- .../components/owntracks/__init__.py | 2 +- .../components/p1_monitor/__init__.py | 2 +- .../components/panasonic_viera/__init__.py | 2 +- homeassistant/components/peco/__init__.py | 2 +- .../components/philips_js/__init__.py | 2 +- homeassistant/components/pi_hole/__init__.py | 2 +- homeassistant/components/picnic/__init__.py | 2 +- homeassistant/components/plaato/__init__.py | 2 +- homeassistant/components/plex/__init__.py | 9 +++-- homeassistant/components/plugwise/gateway.py | 2 +- .../components/plum_lightpad/__init__.py | 2 +- .../components/poolsense/__init__.py | 2 +- .../components/powerwall/__init__.py | 2 +- .../components/progettihwsw/__init__.py | 2 +- homeassistant/components/prosegur/__init__.py | 2 +- homeassistant/components/ps4/__init__.py | 2 +- .../components/pure_energie/__init__.py | 2 +- homeassistant/components/pvoutput/__init__.py | 2 +- .../pvpc_hourly_pricing/__init__.py | 2 +- homeassistant/components/qnap_qsw/__init__.py | 2 +- homeassistant/components/rachio/__init__.py | 2 +- .../components/radiotherm/__init__.py | 2 +- .../components/rainforest_eagle/__init__.py | 2 +- .../components/rainmachine/__init__.py | 2 +- homeassistant/components/rdw/__init__.py | 2 +- .../components/recollect_waste/__init__.py | 2 +- homeassistant/components/renault/__init__.py | 2 +- homeassistant/components/rfxtrx/__init__.py | 2 +- homeassistant/components/ridwell/__init__.py | 2 +- homeassistant/components/ring/__init__.py | 2 +- homeassistant/components/risco/__init__.py | 13 ++----- .../rituals_perfume_genie/__init__.py | 2 +- homeassistant/components/roku/__init__.py | 2 +- homeassistant/components/roomba/__init__.py | 2 +- homeassistant/components/roon/server.py | 4 ++- .../components/rpi_power/__init__.py | 2 +- .../components/ruckus_unleashed/__init__.py | 2 +- homeassistant/components/sabnzbd/__init__.py | 2 +- .../components/screenlogic/__init__.py | 2 +- homeassistant/components/season/__init__.py | 2 +- homeassistant/components/sense/__init__.py | 2 +- homeassistant/components/senseme/__init__.py | 2 +- homeassistant/components/sensibo/__init__.py | 2 +- homeassistant/components/senz/__init__.py | 2 +- homeassistant/components/sharkiq/__init__.py | 2 +- homeassistant/components/sia/__init__.py | 2 +- homeassistant/components/sia/hub.py | 2 +- .../components/simplisafe/__init__.py | 2 +- homeassistant/components/skybell/__init__.py | 2 +- homeassistant/components/sleepiq/__init__.py | 2 +- .../components/slimproto/__init__.py | 2 +- homeassistant/components/sma/__init__.py | 2 +- homeassistant/components/smappee/__init__.py | 2 +- .../components/smart_meter_texas/__init__.py | 2 +- .../components/smartthings/__init__.py | 2 +- homeassistant/components/smarttub/__init__.py | 2 +- homeassistant/components/smhi/__init__.py | 2 +- homeassistant/components/sms/__init__.py | 2 +- .../components/solaredge/__init__.py | 2 +- homeassistant/components/solarlog/__init__.py | 2 +- homeassistant/components/solax/__init__.py | 2 +- homeassistant/components/soma/__init__.py | 2 +- .../components/somfy_mylink/__init__.py | 2 +- homeassistant/components/sonarr/__init__.py | 2 +- homeassistant/components/songpal/__init__.py | 2 +- homeassistant/components/sonos/__init__.py | 9 ++--- .../components/speedtestdotnet/__init__.py | 2 +- homeassistant/components/spider/__init__.py | 2 +- homeassistant/components/spotify/__init__.py | 2 +- homeassistant/components/sql/__init__.py | 2 +- .../components/squeezebox/__init__.py | 2 +- .../components/srp_energy/__init__.py | 2 +- homeassistant/components/starline/__init__.py | 2 +- homeassistant/components/steamist/__init__.py | 2 +- .../components/stookalert/__init__.py | 2 +- homeassistant/components/subaru/__init__.py | 2 +- .../components/surepetcare/__init__.py | 2 +- .../components/switch_as_x/__init__.py | 2 +- .../components/switchbot/__init__.py | 4 ++- .../components/switcher_kis/__init__.py | 25 +++++--------- .../components/syncthing/__init__.py | 2 +- homeassistant/components/syncthru/__init__.py | 2 +- .../components/synology_dsm/__init__.py | 2 +- .../components/system_bridge/__init__.py | 2 +- homeassistant/components/tado/__init__.py | 2 +- .../components/tailscale/__init__.py | 2 +- .../components/tankerkoenig/__init__.py | 2 +- homeassistant/components/tasmota/__init__.py | 21 ++++-------- homeassistant/components/tautulli/__init__.py | 2 +- .../tesla_wall_connector/__init__.py | 2 +- .../components/threshold/__init__.py | 4 ++- homeassistant/components/tibber/__init__.py | 2 +- homeassistant/components/tile/__init__.py | 2 +- homeassistant/components/tod/__init__.py | 4 ++- homeassistant/components/tolo/__init__.py | 2 +- homeassistant/components/toon/__init__.py | 2 +- homeassistant/components/tplink/__init__.py | 2 +- homeassistant/components/traccar/__init__.py | 2 +- homeassistant/components/tractive/__init__.py | 2 +- homeassistant/components/tradfri/__init__.py | 2 +- .../components/trafikverket_ferry/__init__.py | 2 +- .../components/trafikverket_train/__init__.py | 2 +- .../trafikverket_weatherstation/__init__.py | 2 +- .../components/transmission/__init__.py | 4 ++- homeassistant/components/tuya/__init__.py | 2 +- .../components/twentemilieu/__init__.py | 2 +- homeassistant/components/twinkly/__init__.py | 2 +- .../components/ukraine_alarm/__init__.py | 2 +- .../components/unifiprotect/__init__.py | 2 +- homeassistant/components/upb/__init__.py | 2 +- homeassistant/components/upcloud/__init__.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- homeassistant/components/uptime/__init__.py | 2 +- .../components/uptimerobot/__init__.py | 2 +- .../components/utility_meter/__init__.py | 4 +-- homeassistant/components/vallox/__init__.py | 2 +- homeassistant/components/velbus/__init__.py | 2 +- homeassistant/components/venstar/__init__.py | 2 +- homeassistant/components/vera/__init__.py | 2 +- homeassistant/components/verisure/__init__.py | 2 +- homeassistant/components/version/__init__.py | 2 +- homeassistant/components/vesync/__init__.py | 11 +++--- homeassistant/components/vicare/__init__.py | 2 +- homeassistant/components/vilfo/__init__.py | 2 +- homeassistant/components/vizio/__init__.py | 2 +- .../components/vlc_telnet/__init__.py | 2 +- homeassistant/components/volumio/__init__.py | 2 +- homeassistant/components/vulcan/__init__.py | 2 +- homeassistant/components/wallbox/__init__.py | 2 +- homeassistant/components/watttime/__init__.py | 2 +- .../components/waze_travel_time/__init__.py | 2 +- homeassistant/components/webostv/__init__.py | 2 +- .../components/whirlpool/__init__.py | 2 +- homeassistant/components/whois/__init__.py | 2 +- homeassistant/components/wiffi/__init__.py | 2 +- homeassistant/components/wilight/__init__.py | 2 +- homeassistant/components/withings/__init__.py | 2 +- homeassistant/components/wiz/__init__.py | 2 +- homeassistant/components/wled/__init__.py | 2 +- homeassistant/components/wolflink/__init__.py | 2 +- homeassistant/components/ws66i/__init__.py | 2 +- homeassistant/components/xbox/__init__.py | 2 +- .../components/xiaomi_aqara/__init__.py | 2 +- .../components/xiaomi_miio/__init__.py | 7 ++-- .../components/yale_smart_alarm/__init__.py | 2 +- .../components/yamaha_musiccast/__init__.py | 2 +- homeassistant/components/yeelight/__init__.py | 2 +- homeassistant/components/yolink/__init__.py | 2 +- homeassistant/components/youless/__init__.py | 2 +- homeassistant/components/zerproc/__init__.py | 2 +- homeassistant/config_entries.py | 18 ++++++++-- homeassistant/setup.py | 6 ++-- 386 files changed, 528 insertions(+), 557 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index edd799d31f7..bd66aa66c9c 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = AbodeSystem(abode, polling) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await setup_hass_events(hass) await hass.async_add_executor_job(setup_hass_services, hass) diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 70db587bd49..d36ea4e8466 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py index f9599a240e8..0bb7cbdc177 100644 --- a/homeassistant/components/acmeda/__init__.py +++ b/homeassistant/components/acmeda/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return False hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/adax/__init__.py b/homeassistant/components/adax/__init__.py index dc24e741c89..438f1fc74ad 100644 --- a/homeassistant/components/adax/__init__.py +++ b/homeassistant/components/adax/__init__.py @@ -10,7 +10,7 @@ PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Adax from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 2a244a5fe80..2bb15d19223 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def add_url(call: ServiceCall) -> None: """Service call to add a new filter subscription to AdGuard Home.""" diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 12c5a4593c5..d50224698b8 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "async_change": async_change, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index a914a23a0da..032e0a3a9f6 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index a2831fe301e..6723d62e9e0 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b sw_version=agent_client.version, ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 31396ecf51b..a3feab1e6f8 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove air_quality entities from registry if they exist ent_reg = er.async_get(hass) diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 9c6babe1136..7c26cded4de 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index 352c0249637..423e890a855 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airtouch4/__init__.py b/homeassistant/components/airtouch4/__init__.py index 7b0673ecfe4..a2c3f716ab1 100644 --- a/homeassistant/components/airtouch4/__init__.py +++ b/homeassistant/components/airtouch4/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index d0e1a741f2e..c1bb6020b3c 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -274,7 +274,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if CONF_API_KEY in entry.data: async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY]) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index cf57a28a5e1..65ce9193e07 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index af996c9f5b2..1a28a03cf05 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady("Can not connect to host") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index d5c1b88e08e..7206b24632b 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -131,7 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await open_connection() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py index 4481afb09ca..ee311df75fa 100644 --- a/homeassistant/components/ambee/__init__.py +++ b/homeassistant/components/ambee/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await pollen.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id][SERVICE_POLLEN] = pollen - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/amberelectric/__init__.py b/homeassistant/components/amberelectric/__init__.py index 0d39077f2f1..b6901e1b81b 100644 --- a/homeassistant/components/amberelectric/__init__.py +++ b/homeassistant/components/amberelectric/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = AmberUpdateCoordinator(hass, api_instance, site_id) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index acab01e30d1..240c9780cee 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -41,5 +41,5 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ambiclimate from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 4b203fc3757..6942ff7ffd0 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -149,7 +149,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ANDROID_DEV_OPT: entry.options.copy(), } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 5177c6f3486..5d9c1cde785 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -67,17 +67,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) ) - async def setup_platforms(): - """Set up platforms and initiate connection.""" - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - await manager.init() - - hass.async_create_task(setup_platforms()) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await manager.init() return True diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index a82e0239842..c2c6be0db30 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL)) tasks[entry.entry_id] = task - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aseko_pool_live/__init__.py b/homeassistant/components/aseko_pool_live/__init__.py index 213d0dabc91..70a66251bdc 100644 --- a/homeassistant/components/aseko_pool_live/__init__.py +++ b/homeassistant/components/aseko_pool_live/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id].append((unit, coordinator)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 0532dd9c7cc..f3d12c3bd39 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ASUSWRT: router} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 6eb17fded3f..56b2c1e969e 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=atag.id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index e8df7e1072d..81842f995e8 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -88,7 +88,7 @@ async def async_setup_august( data = hass.data[DOMAIN][config_entry.entry_id] = AugustData(hass, august_gateway) await data.async_setup() - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 01d6092a4f2..b8d0589d007 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: AURORA_API: api, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index c988121b6bd..305a42d4dcc 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.data[CONF_ADDRESS] ser_client = AuroraSerialClient(address, comport, parity="N", timeout=1) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ser_client - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index 6136ff6f8f3..b32e11f27c0 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "client": client, "services": services, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index 2364b85a63e..ef39e488001 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index c6de8515979..645932f8b73 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator, project - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 601215d4c61..9bb056ece4f 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_future) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py index 60989ecc6d6..6be1d741137 100644 --- a/homeassistant/components/balboa/__init__.py +++ b/homeassistant/components/balboa/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(stop_monitoring) # At this point we have a configured spa. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def keep_alive(now: datetime) -> None: """Keep alive task.""" diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index a8f93dd0122..383e5a95f4e 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -48,7 +48,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: domain_entry = domain.setdefault(entry.entry_id, {}) product = domain_entry.setdefault(PRODUCT, product) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 7321e2392df..0c18950da66 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.data[DOMAIN][entry.entry_id].available: raise ConfigEntryNotReady - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def blink_refresh(event_time=None): """Call blink to refresh info.""" diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 7023dd7481a..959fbe04240 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -127,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms except notify - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] ) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 7dca4db507d..46066d9f55e 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _async_remove_old_device_identifiers(config_entry_id, device_registry, hub) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index 4d076a784d1..a8b2a389f9f 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=shc_info.version, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def stop_polling(event): """Stop polling service.""" diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 3962e953520..e1d90681d2a 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 46582334e2d..c279e7860b6 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -126,7 +126,7 @@ class BroadlinkDevice: self.reset_jobs.append(config.add_update_listener(self.async_update)) # Forward entry setup to related domains. - self.hass.config_entries.async_setup_platforms( + await self.hass.config_entries.async_forward_entry_setups( config, get_domains(self.api.type) ) diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 96e2ad069ce..7c3d3003ea6 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator hass.data[DOMAIN][SNMP] = snmp_engine - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 988a96ce08e..f189be63920 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_BAPI: bapi, DATA_COOR: coordinator} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 7ada5b01e46..c15324825ba 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_BSBLAN_CLIENT: bsblan} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/buienradar/__init__.py b/homeassistant/components/buienradar/__init__.py index 64b94cfbaa8..9055220b7ee 100644 --- a/homeassistant/components/buienradar/__init__.py +++ b/homeassistant/components/buienradar/__init__.py @@ -13,7 +13,7 @@ PLATFORMS = [Platform.CAMERA, Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up buienradar from a config entry.""" hass.data.setdefault(DOMAIN, {}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 9b020a2f09d..e6ea20e0768 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -116,7 +116,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 95f1b660520..85f73532fed 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_finish_startup(_): await coordinator.async_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.state == CoreState.running: await async_finish_startup(None) diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index cf80b83fc36..4f2ad7c5889 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -159,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index ba613ea6b18..cc7738741bb 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 4ef26a11130..36ce65517db 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = instance - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 48a639e56f6..4253a4eca02 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data[CONF_CONFIG_LISTENER] = entry.add_update_listener(update_listener) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 0f98f8d3afc..ef2b5328f96 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_INFO: info, DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index 27085c88ef2..a1c4f876f66 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not coordinator.last_update_success: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/cpuspeed/__init__.py b/homeassistant/components/cpuspeed/__init__.py index a5d6a35459f..da1e0129117 100644 --- a/homeassistant/components/cpuspeed/__init__.py +++ b/homeassistant/components/cpuspeed/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/crownstone/entry_manager.py b/homeassistant/components/crownstone/entry_manager.py index b1963462adc..dcae7ef4705 100644 --- a/homeassistant/components/crownstone/entry_manager.py +++ b/homeassistant/components/crownstone/entry_manager.py @@ -97,7 +97,9 @@ class CrownstoneEntryManager: # Makes HA aware of the Crownstone environment HA is placed in, a user can have multiple self.usb_sphere_id = self.config_entry.options[CONF_USB_SPHERE] - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) # HA specific listeners self.config_entry.async_on_unload( diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 5f05355374c..536e2fa48d1 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index 2253eee43d5..2958b9e1df6 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = DelugeDataUpdateCoordinator(hass, api, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index d6c5a5d3afc..2602d674005 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -259,10 +259,9 @@ async def _insert_statistics(hass): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set the config entry up.""" # Set up demo platforms with config entry - for platform in COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + await hass.config_entries.async_forward_entry_setups( + config_entry, COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM + ) if "recorder" in hass.config.components: await _insert_statistics(hass) return True diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 27594703fd2..0f58f5f5218 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index e3fe9d85f41..c5b1c8e31e9 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Derivative from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 24075ee1a7d..e9222156b00 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -47,6 +47,14 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the device tracker.""" + + # We need to add the component here break the deadlock + # when setting up integrations from config entries as + # they would otherwise wait for the device tracker to be + # setup and thus the config entries would not be able to + # setup their platforms. + hass.config.components.add(DOMAIN) + await async_setup_legacy_integration(hass, config) return True diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index b79d0a5c8fe..ec0d5a3a666 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except GatewayOfflineError as err: raise ConfigEntryNotReady from err - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def shutdown(event: Event) -> None: for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index f427e5acbfc..5cf91325d70 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for coordinator in coordinators.values(): await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 7b5ae85bdff..137a884d201 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: COORDINATOR ].async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 1068ec4ccc4..3dfb5708b98 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = dtv - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dlna_dmr/__init__.py b/homeassistant/components/dlna_dmr/__init__.py index d34d8550355..e9dd60c5896 100644 --- a/homeassistant/components/dlna_dmr/__init__.py +++ b/homeassistant/components/dlna_dmr/__init__.py @@ -17,7 +17,7 @@ async def async_setup_entry( LOGGER.debug("Setting up config entry: %s", entry.unique_id) # Forward setup to the appropriate platform - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dnsip/__init__.py b/homeassistant/components/dnsip/__init__.py index f679fb4ad30..13783a1b07f 100644 --- a/homeassistant/components/dnsip/__init__.py +++ b/homeassistant/components/dnsip/__init__.py @@ -10,7 +10,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DNS IP from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) return True diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 133c7612a89..62554f9662c 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -146,7 +146,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 0e238363fc0..044cfd14e64 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -13,7 +13,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index 839f79bc3f4..c97e27f4017 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = player - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 35f76354eab..fe1872e1fe3 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -275,7 +275,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = None raise ConfigEntryNotReady - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/eafm/__init__.py b/homeassistant/components/eafm/__init__.py index e7b6cd88092..a57558ff1cc 100644 --- a/homeassistant/components/eafm/__init__.py +++ b/homeassistant/components/eafm/__init__.py @@ -11,7 +11,7 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up flood monitoring sensors for this config entry.""" hass.data.setdefault(DOMAIN, {}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index ceab670fa6e..7204dbf8de2 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = data - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index a706ceb8e7e..728222dcda7 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api hass.data[DOMAIN][EQUIPMENT][config_entry.entry_id] = equipment - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) api.subscribe() diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 915eb0daf46..ce8483672a2 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 5cd7bec9244..67ff6c59a54 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: eight, heat_coordinator, user_coordinator ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index f15ccc0a03d..c2d70c69735 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: info=info, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index cf1cac3bdb3..00fcadfe57a 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -303,7 +303,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "keypads": {}, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/elmax/__init__.py b/homeassistant/components/elmax/__init__.py index af123efae9a..0c0a80b4958 100644 --- a/homeassistant/components/elmax/__init__.py +++ b/homeassistant/components/elmax/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator # Perform platform initialization. - hass.config_entries.async_setup_platforms(entry, ELMAX_PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, ELMAX_PLATFORMS) return True diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 3d03c7b8fe6..ea19808cd37 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 0c6c893df64..61c2fd86c77 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: NAME: name, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 4ed1f5d0fbb..e12d12b87d0 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinators - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index 9710cb8d96a..5a8544c7bf1 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = projector - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index ddedaf11ceb..bddb56a38d3 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -321,20 +321,17 @@ async def async_setup_entry( # noqa: C901 on_connect_error=on_connect_error, ) - async def complete_setup() -> None: - """Complete the config entry setup.""" - infos, services = await entry_data.async_load_from_store() - await entry_data.async_update_static_infos(hass, entry, infos) - await _setup_services(hass, entry_data, services) + infos, services = await entry_data.async_load_from_store() + await entry_data.async_update_static_infos(hass, entry, infos) + await _setup_services(hass, entry_data, services) - if entry_data.device_info is not None and entry_data.device_info.name: - cli.expected_name = entry_data.device_info.name - reconnect_logic.name = entry_data.device_info.name + if entry_data.device_info is not None and entry_data.device_info.name: + cli.expected_name = entry_data.device_info.name + reconnect_logic.name = entry_data.device_info.name - await reconnect_logic.start() - entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) + await reconnect_logic.start() + entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) - hass.async_create_task(complete_setup()) return True diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 41a0e89245e..80fd855379e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -101,13 +101,8 @@ class RuntimeEntryData: ) -> None: async with self.platform_load_lock: needed = platforms - self.loaded_platforms - tasks = [] - for platform in needed: - tasks.append( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) - if tasks: - await asyncio.wait(tasks) + if needed: + await hass.config_entries.async_forward_entry_setups(entry, needed) self.loaded_platforms |= needed async def async_update_static_infos( diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index da3cbb51717..d7083715394 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 9d6f7864b84..51931f4b104 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -83,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, DATA_UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 8bfcf60f30a..10ddb13c228 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 3b5eece1a14..9431cd162bc 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -519,7 +519,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: configuration_url=controller.hub_api_url.removesuffix("/api/"), ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) controller.enable_state_handler() diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 61ef26f0a66..9e08615d4ab 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.config.is_allowed_path(path): raise ConfigEntryNotReady(f"Filepath {path} is not valid or allowed") - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index ffd82307940..a9a4323fe12 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index ffc4d39a89b..a58cd0591d1 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -197,11 +197,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b sw_version=board.firmware_version, ) - for (conf, platform) in CONF_PLATFORM_MAP.items(): - if conf in config_entry.data: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + await hass.config_entries.async_forward_entry_setups( + config_entry, + [ + platform + for conf, platform in CONF_PLATFORM_MAP.items() + if conf in config_entry.data + ], + ) return True diff --git a/homeassistant/components/fivem/__init__.py b/homeassistant/components/fivem/__init__.py index 1fe5ccf0b8f..7b0ae2e2758 100644 --- a/homeassistant/components/fivem/__init__.py +++ b/homeassistant/components/fivem/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 54eaf5a6917..a963d199c5a 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = FlickAPI(auth) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 3281410ec2d..3f9f70e294c 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 2dcca979acc..b30e31de361 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: tasks = [device.async_refresh() for device in devices] await asyncio.gather(*tasks) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 3ca99a335f2..294f50c50e2 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: FLUME_HTTP_SESSION: http_session, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 9a2ef2d4465..5e48e1561b5 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index e6c1393154a..0284bf90ba0 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -187,7 +187,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = FluxLedUpdateCoordinator(hass, device, entry) hass.data[DOMAIN][entry.entry_id] = coordinator platforms = PLATFORMS_BY_TYPE[device.device_type] - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) async def _async_sync_time(*args: Any) -> None: """Set the time every morning at 02:40:30.""" diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 18d542c1d3b..ece451a9b0a 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 903a56ce559..14f40db2057 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -10,7 +10,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up forked-daapd from a config entry by forwarding to platform.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 2adac19c1de..ef88d0f671a 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -21,7 +21,7 @@ PLATFORMS = [Platform.CAMERA] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up foscam from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 7d7bc7695cd..12463934adb 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = router - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Services async def async_reboot(call: ServiceCall) -> None: diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index ec0085c9d32..6bd4d03bde0 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 55253fe3d1e..28036ef37e7 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await avm_wrapper.async_config_entry_first_refresh() # Load the other platforms like switch - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await async_setup_services(hass) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 7bb71e52560..4bb2eee45b1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_migrate_entries(hass, entry.entry_id, _update_unique_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def logout_fritzbox(event: Event) -> None: """Close connections to this fritzbox.""" diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py index 812d7a7db59..a47c3c24755 100644 --- a/homeassistant/components/fritzbox_callmonitor/__init__.py +++ b/homeassistant/components/fritzbox_callmonitor/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index f6607aed11f..c4d764f4c71 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await solar_net.init_devices() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = solar_net - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 01dc6b17545..5f8f3e36671 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -19,7 +19,7 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Garages Amsterdam from a config entry.""" await get_coordinator(hass) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py index b585658befb..56f17adc992 100644 --- a/homeassistant/components/gdacs/__init__.py +++ b/homeassistant/components/gdacs/__init__.py @@ -136,7 +136,9 @@ class GdacsFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) async def update(event_time): """Update.""" diff --git a/homeassistant/components/generic/__init__.py b/homeassistant/components/generic/__init__.py index cb669d8b906..280b468add8 100644 --- a/homeassistant/components/generic/__init__.py +++ b/homeassistant/components/generic/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up generic IP camera from a config entry.""" await _async_migrate_unique_ids(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/geocaching/__init__.py b/homeassistant/components/geocaching/__init__.py index 430cbc9a8d0..aa2926df949 100644 --- a/homeassistant/components/geocaching/__init__.py +++ b/homeassistant/components/geocaching/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index d3b0fcbe81a..2e6ed8429dd 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -144,7 +144,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index d9c27e9dff8..6c091e71f05 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -144,7 +144,9 @@ class GeonetnzQuakesFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) async def update(event_time): """Update.""" diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index 8d0733aacd1..a1b6368c8ef 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -130,7 +130,9 @@ class GeonetnzVolcanoFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) async def update(event_time): """Update.""" diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 73d773561f5..adef70e5864 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove air_quality entities from registry if they exist ent_reg = er.async_get(hass) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 404aeae11b5..53b8cd67871 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_cleanup_device_registry(hass=hass, entry=entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 571214deb20..2b52cdeef8b 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -91,7 +91,9 @@ class GlancesData: self.config_entry.add_update_listener(self.async_options_updated) ) - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) return True diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index ea292a651c2..f32cad5a488 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = GoalZeroDataUpdateCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index 7dccd5551c7..ece2f6bbbc8 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data_update_coordinator = get_data_update_coordinator(hass, entry) await data_update_coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/goodwe/__init__.py b/homeassistant/components/goodwe/__init__.py index e48590931ba..5dcc66e90d4 100644 --- a/homeassistant/components/goodwe/__init__.py +++ b/homeassistant/components/goodwe/__init__.py @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 261c61d1a88..e05de4f5b79 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -272,7 +272,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if get_feature_access(hass, entry) is FeatureAccess.read_write: await async_setup_add_event_service(hass, calendar_service) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 2012e38e0a2..7d125be5025 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 3df0bac51e9..5331f6e7029 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index fa51a48bb4f..d4a929f1642 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery hass.data[DOMAIN].setdefault(DISPATCHERS, []) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def _async_scan_update(_=None): await gree_discovery.discovery.scan() diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 1f8fba21e78..943b15dd870 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -231,7 +231,9 @@ def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - hass.config_entries.async_setup_platforms(entry, (entry.options["group_type"],)) + await hass.config_entries.async_forward_entry_setups( + entry, (entry.options["group_type"],) + ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/growatt_server/__init__.py b/homeassistant/components/growatt_server/__init__.py index f77323bf536..177d0957883 100644 --- a/homeassistant/components/growatt_server/__init__.py +++ b/homeassistant/components/growatt_server/__init__.py @@ -11,7 +11,7 @@ async def async_setup_entry( ) -> bool: """Load the saved entities.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index ba4bc5e6ad6..b07d8ec5b3f 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -161,7 +161,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Set up all of the Guardian entity platforms: - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def extract_client(func: Callable) -> Callable: diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 3cadd6897d2..25738893689 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -155,7 +155,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) data[entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): hass.services.async_register( diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 4e109ae95a7..259ea660317 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CANCEL_STOP: cancel_stop, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 5b5cc48eed8..d580847646d 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -758,7 +758,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[ADDONS_COORDINATOR] = coordinator await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 24da0bc2673..549cc08356c 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b here_travel_time_config, ) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py index 74eb4371614..cc599aa31fc 100644 --- a/homeassistant/components/hisense_aehw4a1/__init__.py +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -75,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for Hisense AEH-W4A1.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 3693f0183ce..a1a784162e8 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -93,12 +93,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except HiveReauthRequired as err: raise ConfigEntryAuthFailed from err - for ha_type, hive_type in PLATFORM_LOOKUP.items(): - device_list = devices.get(hive_type) - if device_list: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, ha_type) - ) + await hass.config_entries.async_forward_entry_setups( + entry, + [ + ha_type + for ha_type, hive_type in PLATFORM_LOOKUP.items() + if devices.get(hive_type) + ], + ) return True diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index ddd9c5da359..c695f5524c3 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -96,29 +96,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.warning("HLK-SW16 %s connected", address) async_dispatcher_send(hass, f"hlk_sw16_device_available_{entry.entry_id}", True) - async def connect(): - """Set up connection and hook it into HA for reconnect/shutdown.""" - _LOGGER.info("Initiating HLK-SW16 connection to %s", address) + _LOGGER.debug("Initiating HLK-SW16 connection to %s", address) - client = await create_hlk_sw16_connection( - host=host, - port=port, - disconnect_callback=disconnected, - reconnect_callback=reconnected, - loop=hass.loop, - timeout=CONNECTION_TIMEOUT, - reconnect_interval=DEFAULT_RECONNECT_INTERVAL, - keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, - ) + client = await create_hlk_sw16_connection( + host=host, + port=port, + disconnect_callback=disconnected, + reconnect_callback=reconnected, + loop=hass.loop, + timeout=CONNECTION_TIMEOUT, + reconnect_interval=DEFAULT_RECONNECT_INTERVAL, + keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, + ) - hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client + hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client - # Load entities - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Load entities + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - _LOGGER.info("Connected to HLK-SW16 device: %s", address) - - hass.loop.create_task(connect()) + _LOGGER.debug("Connected to HLK-SW16 device: %s", address) return True diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 50e51b4ae1e..a2876dd86f5 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -263,7 +263,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await update_all_devices(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index fbae1ae970a..d58086e59ec 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -1,5 +1,4 @@ """The Legrand Home+ Control integration.""" -import asyncio from datetime import timedelta import logging @@ -145,18 +144,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(coordinator.async_add_listener(_async_update_entities)) - async def start_platforms(): - """Continue setting up the platforms.""" - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - # Only refresh the coordinator after all platforms are loaded. - await coordinator.async_refresh() + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - hass.async_create_task(start_platforms()) + # Only refresh the coordinator after all platforms are loaded. + await coordinator.async_refresh() return True diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index 4a087988b61..ec43cdfdd2e 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -75,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index bad0ed96e01..146b8e3c035 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -77,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await data.async_update() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = data - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index c0337095a9c..3ca3daff523 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -434,7 +434,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Forward config entry setup to platforms - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Notify doesn't support config entry setup yet, load with discovery for now await discovery.async_load_platform( diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index e15da5c8489..625a623105f 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -100,7 +100,7 @@ class HueBridge: if self.api_version == 1: if self.api.sensors is not None: self.sensor_manager = SensorManager(self) - self.hass.config_entries.async_setup_platforms( + await self.hass.config_entries.async_forward_entry_setups( self.config_entry, PLATFORMS_v1 ) @@ -108,7 +108,7 @@ class HueBridge: else: await async_setup_devices(self) await async_setup_hue_events(self) - self.hass.config_entries.async_setup_platforms( + await self.hass.config_entries.async_forward_entry_setups( self.config_entry, PLATFORMS_v2 ) diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index f2b4ef8d4ef..fa810d823ca 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_COORDINATOR: coordinator} # Offload the loading of entities to the platform - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 4a22bc4ed81..f7c8cd1eb4b 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_info=device_info, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index 3c090249bc0..1104359111c 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = hub - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 292c426ba19..ec478883e8f 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -269,21 +269,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } ) - async def setup_then_listen() -> None: - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - assert hyperion_client - if hyperion_client.instances is not None: - await async_instances_to_clients_raw(hyperion_client.instances) - hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append( - entry.add_update_listener(_async_entry_updated) - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + assert hyperion_client + if hyperion_client.instances is not None: + await async_instances_to_clients_raw(hyperion_client.instances) + hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append( + entry.add_update_listener(_async_entry_updated) + ) - hass.async_create_task(setup_then_listen()) return True diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index 254bf6f685f..374ba29dccf 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 02e27d8e82f..844313a4aed 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -117,22 +117,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: elif isinstance(dev, AqualinkToggle): switches += [dev] - forward_setup = hass.config_entries.async_forward_entry_setup + platforms = [] if binary_sensors: _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors), binary_sensors) - hass.async_create_task(forward_setup(entry, Platform.BINARY_SENSOR)) + platforms.append(Platform.BINARY_SENSOR) if climates: _LOGGER.debug("Got %s climates: %s", len(climates), climates) - hass.async_create_task(forward_setup(entry, Platform.CLIMATE)) + platforms.append(Platform.CLIMATE) if lights: _LOGGER.debug("Got %s lights: %s", len(lights), lights) - hass.async_create_task(forward_setup(entry, Platform.LIGHT)) + platforms.append(Platform.LIGHT) if sensors: _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors) - hass.async_create_task(forward_setup(entry, Platform.SENSOR)) + platforms.append(Platform.SENSOR) if switches: _LOGGER.debug("Got %s switches: %s", len(switches), switches) - hass.async_create_task(forward_setup(entry, Platform.SWITCH)) + platforms.append(Platform.SWITCH) + + await hass.config_entries.async_forward_entry_setups(entry, platforms) async def _async_systems_update(now): """Refresh internal state for all systems.""" diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 8175cf43f27..06028ebce6c 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -98,7 +98,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.unique_id] = account - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def play_sound(service: ServiceCall) -> None: """Play sound on the device.""" diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index e2bebf0bb7f..82b910215c4 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -154,10 +154,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) device = devices.add_x10_device(housecode, unitcode, x10_type, steps) - for platform in INSTEON_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + await hass.config_entries.async_forward_entry_setups(entry, INSTEON_PLATFORMS) for address in devices: device = devices[address] diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index 84bc28e3d2f..f482f4e41e8 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Integration from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 5fb8ac92a72..f5b6085781b 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index a70a9908fd8..717effa2bb1 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -287,7 +287,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Set up an iOS entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.http.register_view(iOSIdentifyDeviceView(hass.config.path(CONFIGURATION_FILE))) hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_USER][CONF_PUSH])) diff --git a/homeassistant/components/iotawatt/__init__.py b/homeassistant/components/iotawatt/__init__.py index 55fc701cffd..9f51382f98e 100644 --- a/homeassistant/components/iotawatt/__init__.py +++ b/homeassistant/components/iotawatt/__init__.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = IotawattUpdater(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 1bd948ffa5e..4d675e8cc1d 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -13,7 +13,7 @@ PLATFORMS = [Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPMA station as config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 23ee5adc0e4..42dc2b8d93b 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 42da8aadb6d..686f5b57f05 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py index 997c3fff2a3..476a944be81 100644 --- a/homeassistant/components/iss/__init__.py +++ b/homeassistant/components/iss/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 3cee445b587..301c86827e9 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -204,7 +204,7 @@ async def async_setup_entry( _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def _async_stop_auto_update(event: Event) -> None: diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 0aef8360bdd..3f2565bd8f4 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -47,7 +47,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" await async_start_discovery_service(hass) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 60377d7e85b..483782c948a 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kaleidescape/__init__.py b/homeassistant/components/kaleidescape/__init__.py index a66ae25d436..f074ac640d8 100644 --- a/homeassistant/components/kaleidescape/__init__.py +++ b/homeassistant/components/kaleidescape/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index ecd9ece1bd0..68465c26c45 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index a7637879fc1..ef4e8ebb303 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) update_listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index f5e58f3c6a1..d3c7d4da724 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_REMOVE_LISTENER: remove_stop_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index b431960caef..6c7a7cde523 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = plenticore - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index bf01f27673d..db2baf56f97 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await kraken_data.async_setup() hass.data[DOMAIN] = kraken_data entry.async_on_unload(entry.add_update_listener(async_options_updated)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 39c0d0a5b84..6c8037bdafc 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if DATA_ADDRESSES not in hass.data[DOMAIN]: hass.data[DOMAIN][DATA_ADDRESSES] = set() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/launch_library/__init__.py b/homeassistant/components/launch_library/__init__.py index 14eac4fd6a6..34ee7441351 100644 --- a/homeassistant/components/launch_library/__init__.py +++ b/homeassistant/components/launch_library/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/laundrify/__init__.py b/homeassistant/components/laundrify/__init__.py index 27fc412abab..4bfb84082d5 100644 --- a/homeassistant/components/laundrify/__init__.py +++ b/homeassistant/components/laundrify/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "coordinator": coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index d1486fe0d32..8df579fddd7 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -120,7 +120,7 @@ async def async_setup_entry( register_lcn_address_devices(hass, config_entry) # forward config_entry to components - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) # register for LCN bus messages device_registry = dr.async_get(hass) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index df672c07143..b6710064a74 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -50,7 +50,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LIFX from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index d854e922ebd..5131ee52e67 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = system - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 90c432010d7..a2612966a98 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from ex if hub.account.robots: - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index cc122187f47..b5ed762ef5d 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -10,7 +10,7 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up local_ip from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index fa4d510d648..5a796b976ff 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -121,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "Locative", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index fff086b6c4e..cabf6342fac 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -190,7 +190,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_LOGI] = logi_circle - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def service_handler(service: ServiceCall) -> None: """Dispatch service calls to target entities.""" diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index 9b0a5b05f1f..5ab4078eb20 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -164,7 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_coordinators=device_coordinators, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index d29a358fb61..d842fdc1a89 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 7faf70f0d7a..cea069a1556 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -188,7 +188,7 @@ async def async_setup_entry( bridge, bridge_device, button_devices ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 2eaee440ae7..e7fe6789268 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -148,7 +148,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 85b9700a624..f5685155587 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -192,7 +192,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() # Setup components - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Register services hass.services.async_register( diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py index 904d9e412c0..6db3093567d 100644 --- a/homeassistant/components/meater/__init__.py +++ b/homeassistant/components/meater/__init__.py @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "coordinator": coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 1bb65a943f8..d8a044d23f6 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: conf = entry.data mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 2f663df458e..b4889a62411 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py index 399e72d4924..a5b096b5554 100644 --- a/homeassistant/components/met_eireann/__init__.py +++ b/homeassistant/components/met_eireann/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index bbc3b16875d..1d36746413e 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 58e51d0490a..7510c4bec4c 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 3d10fdef378..e71c417da43 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: metoffice_daily_coordinator.async_config_entry_first_refresh(), ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 6131ebc7fd4..8fd1d1a3e22 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][conn_type][key] = data_coordinator await data_coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/min_max/__init__.py b/homeassistant/components/min_max/__init__.py index db80473f90a..a027a029ec2 100644 --- a/homeassistant/components/min_max/__init__.py +++ b/homeassistant/components/min_max/__init__.py @@ -9,7 +9,7 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Min/Max from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index b45cc0fb2e3..1b4a71e8ab8 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: server.start_periodic_update() # Set up platforms. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mjpeg/__init__.py b/homeassistant/components/mjpeg/__init__.py index 632156b7adc..605c8b6c9d5 100644 --- a/homeassistant/components/mjpeg/__init__.py +++ b/homeassistant/components/mjpeg/__init__.py @@ -24,7 +24,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Reload entry when its updated. entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 1b308705624..7aac961042b 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: registration_name = f"Mobile App: {registration[ATTR_DEVICE_NAME]}" webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass_notify.async_reload(hass, DOMAIN) diff --git a/homeassistant/components/modem_callerid/__init__.py b/homeassistant/components/modem_callerid/__init__.py index 8f62cf4beb5..bbdd1b05383 100644 --- a/homeassistant/components/modem_callerid/__init__.py +++ b/homeassistant/components/modem_callerid/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Unable to open port: {device}") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index ed4212d9444..9b425f61ad0 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index 64bdfeb4e6d..b2d3438250f 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 91fd353f2e0..1c9a2fa7868 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: FIRST_RUN: first_run, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/moon/__init__.py b/homeassistant/components/moon/__init__.py index 0b36ba59198..e2eaaf89948 100644 --- a/homeassistant/components/moon/__init__.py +++ b/homeassistant/components/moon/__init__.py @@ -7,7 +7,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 95ac1c5fd44..184e721aeed 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -204,7 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=version, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 279cc8bde70..462f5ef03d2 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -1,7 +1,6 @@ """The motionEye integration.""" from __future__ import annotations -import asyncio from collections.abc import Callable import contextlib from http import HTTPStatus @@ -392,20 +391,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: device_registry.async_remove_device(device_entry.id) - async def setup_then_listen() -> None: - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - entry.async_on_unload( - coordinator.async_add_listener(_async_process_motioneye_cameras) - ) - await coordinator.async_refresh() - entry.async_on_unload(entry.add_update_listener(_async_entry_updated)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload( + coordinator.async_add_listener(_async_process_motioneye_cameras) + ) + await coordinator.async_refresh() + entry.async_on_unload(entry.add_update_listener(_async_entry_updated)) - hass.async_create_task(setup_then_listen()) return True diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a099e7b580c..143d97b928f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -438,7 +438,7 @@ async def async_setup_entry( # noqa: C901 blocking=False, ) - hass.async_create_task(async_forward_entry_setup_and_setup_discovery(entry)) + await async_forward_entry_setup_and_setup_discovery(entry) return True diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 5d558ee0ade..7934220cf91 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py index 4cdd97f446b..aa5e0d70fe9 100644 --- a/homeassistant/components/mutesync/__init__.py +++ b/homeassistant/components/mutesync/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 8bdf07dad75..d5b4730c2de 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 3313a70808c..cc0d918ec05 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -1,7 +1,6 @@ """Connect to a MySensors gateway via pymysensors API.""" from __future__ import annotations -import asyncio from collections.abc import Callable from functools import partial import logging @@ -86,16 +85,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), ) - async def finish() -> None: - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS_WITH_ENTRY_SUPPORT - ) - ) - await finish_setup(hass, entry, gateway) - - hass.async_create_task(finish()) + await hass.config_entries.async_forward_entry_setups( + entry, PLATFORMS_WITH_ENTRY_SUPPORT + ) + await finish_setup(hass, entry, gateway) return True diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 021b46e2f38..302c0af76d4 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove air_quality entities from registry if they exist ent_reg = entity_registry.async_get(hass) diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index f6fb2f8112b..1d18e0078ec 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nanoleaf, coordinator, event_listener ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index cbfd860a0b1..e7b402aed36 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[NEATO_LOGIN] = hub - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index b31354b598c..72759ac0f52 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -227,7 +227,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_DEVICE_MANAGER: device_manager, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index a3a9d22e017..36847c85515 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -153,7 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await data_handler.async_setup() hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def unregister_webhook( call_or_event_or_dt: ServiceCall | Event | datetime | None, diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index a996699ab9e..ade5a8df6bd 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -160,7 +160,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: KEY_COORDINATOR_LINK: coordinator_link, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 355c17a2ed1..b221f440ff8 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 8e0c6c2b791..88f12ffa4bc 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_type=dr.DeviceEntryType.SERVICE, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index c5375c96785..17e0280ca50 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 21469f197f4..5f3333ec750 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner = domain_data[entry.entry_id] = NmapDeviceScanner(hass, entry, devices) await scanner.async_setup() entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index bc8a7bffa95..788c698c0ca 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -39,6 +39,13 @@ PLATFORM_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" + + # We need to add the component here break the deadlock + # when setting up integrations from config entries as + # they would otherwise wait for notify to be + # setup and thus the config entries would not be able to + # setup their platforms. + hass.config.components.add(DOMAIN) await async_setup_legacy(hass, config) async def persistent_notification(service: ServiceCall) -> None: diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index e8bd2c725d5..49277740f56 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 06796e836b4..86574920cd0 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index a59b0a62f70..dcb359f32d2 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -147,7 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 28c41ccda3a..27332e50b18 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=data.device_info.firmware, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 3667acf975e..fe0d0b97c69 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -159,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator_forecast.async_refresh() await coordinator_forecast_hourly.async_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index a29ea829bbc..c5512076172 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) _async_register_services(hass, coordinator) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index eded3bec16a..1d1c1958420 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -182,7 +182,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "client": client, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index 8a55eff6bb0..27f145f82b6 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: OMNI_API: api, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/oncue/__init__.py b/homeassistant/components/oncue/__init__.py index 24a2ec24c7d..eb9ac37db18 100644 --- a/homeassistant/components/oncue/__init__.py +++ b/homeassistant/components/oncue/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index e827b32f48a..5dccca54772 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index b836d7e3298..e3454a5eb5c 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = onewire_hub - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(options_update_listener)) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 7922d59ca53..ac20a564c8a 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if device.capabilities.events: platforms += [Platform.BINARY_SENSOR, Platform.SENSOR] - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py index de42d19d8c9..4dc6a45e16c 100644 --- a/homeassistant/components/open_meteo/__init__.py +++ b/homeassistant/components/open_meteo/__init__.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index eb1b50db5b6..6b97e88df0b 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await open_garage_data_coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = open_garage_data_coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 5ec8c6420d5..76f8341734c 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Schedule directly on the loop to avoid blocking HA startup. hass.loop.create_task(gateway.connect_and_subscribe()) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) register_services(hass) return True diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 774cd05fd9f..be003507b81 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = openuv - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @_verify_domain_control async def update_data(_: ServiceCall) -> None: diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 8fd7aaae7ad..d462e34cd84 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) update_listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 1132e269d04..9acdbfb9ec9 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class): platforms[platform].append(device) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) device_registry = dr.async_get(hass) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 9d2623af0f9..cb2ded6fcef 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -77,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() # Setup components - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 5a21862c767..6086ee1efd8 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: webhook.async_register(hass, DOMAIN, "OwnTracks", webhook_id, handle_webhook) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.data[DOMAIN]["unsub"] = async_dispatcher_connect( hass, DOMAIN, async_handle_message diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index 44a3c855c8c..03055013345 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index e03dca74fb0..79504653a39 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -109,7 +109,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b data={**config, ATTR_DEVICE_INFO: device_info}, ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/peco/__init__.py b/homeassistant/components/peco/__init__.py index 86f69213a1c..ad74200dace 100644 --- a/homeassistant/components/peco/__init__.py +++ b/homeassistant/components/peco/__init__.py @@ -60,7 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 8ff20d8b104..40e803e4b35 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_entry)) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index df156353b88..ffb3352e282 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -135,7 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_KEY_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, _async_platforms(entry)) + await hass.config_entries.async_forward_entry_setups(entry, _async_platforms(entry)) return True diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py index e34223a4799..a7d26ceb5c6 100644 --- a/homeassistant/components/picnic/__init__.py +++ b/homeassistant/components/picnic/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_COORDINATOR: picnic_coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 19c93533867..914d92040d2 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: await async_setup_coordinator(hass, entry) - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, [platform for platform in PLATFORMS if entry.options.get(platform, True)] ) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index c8745213f90..148e6a906bd 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -215,7 +215,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket - def start_websocket_session(platform, _): + def start_websocket_session(platform): hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) if hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: hass.loop.create_task(websocket.listen()) @@ -228,11 +228,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + for platform in PLATFORMS: - task = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) - task.add_done_callback(partial(start_websocket_session, platform)) + start_websocket_session(platform) async_cleanup_plex_devices(hass, entry) diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index afa7451021e..4fde6a54a4a 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -77,7 +77,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=api.smile_version[0], ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS_GATEWAY) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS_GATEWAY) return True diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 83aec34a185..aa82d5662c6 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = plum - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def cleanup(event): """Clean up resources.""" diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 72bfee387eb..a3e9a93da37 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 31e249ec806..2fdf3d61d20 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -174,7 +174,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = runtime_data - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index 1ebabd5bb08..bce25c07b17 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Check board validation again to load new values to API. await hass.data[DOMAIN][entry.entry_id].check_board() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py index b8023c7fadd..04f353e96b8 100644 --- a/homeassistant/components/prosegur/__init__.py +++ b/homeassistant/components/prosegur/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Could not connect with Prosegur backend: %s", error) raise ConfigEntryNotReady from error - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 7e215060d73..e480396e6a2 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -75,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up PS4 from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pure_energie/__init__.py b/homeassistant/components/pure_energie/__init__.py index 4e86726ccc8..4a64e5abb84 100644 --- a/homeassistant/components/pure_energie/__init__.py +++ b/homeassistant/components/pure_energie/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pvoutput/__init__.py b/homeassistant/components/pvoutput/__init__.py index 6457a4d25c2..bca5a23e62b 100644 --- a/homeassistant/components/pvoutput/__init__.py +++ b/homeassistant/components/pvoutput/__init__.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 7ecc89020c0..f10d3b995a8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index 040f8eb52f2..b3bcc1705de 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: QSW_COORD_FW: coord_fw, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index e0ac98b7546..6723678012f 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -95,6 +95,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person async_register_webhook(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index 865e75257ec..787570eaeb4 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_call_or_raise_not_ready(time_coro, host) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/rainforest_eagle/__init__.py b/homeassistant/components/rainforest_eagle/__init__.py index 862c9850cb9..7cf540de1e6 100644 --- a/homeassistant/components/rainforest_eagle/__init__.py +++ b/homeassistant/components/rainforest_eagle/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = data.EagleDataCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index c285bf89e57..7647a330a30 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -255,7 +255,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinators, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/rdw/__init__.py b/homeassistant/components/rdw/__init__.py index ac2bed06310..601d21ee889 100644 --- a/homeassistant/components/rdw/__init__.py +++ b/homeassistant/components/rdw/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index a977ce18f3f..c4c3f9a1e35 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index 17e4d3dd82b..21ce5118401 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][config_entry.entry_id] = renault_hub - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_AC_START): setup_services(hass) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 60ca25ed6d7..efb9b634d62 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 5f3933a09c5..6b615719bd2 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -82,7 +82,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 0d8f87eef3c..fb037eca05d 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.services.has_service(DOMAIN, "update"): return True diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 362fd615700..1932548a907 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -1,5 +1,4 @@ """The Risco integration.""" -import asyncio from datetime import timedelta import logging @@ -56,16 +55,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: EVENTS_COORDINATOR: events_coordinator, } - async def start_platforms(): - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - await events_coordinator.async_refresh() - - hass.async_create_task(start_platforms()) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await events_coordinator.async_refresh() return True diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index c45a762ca9a..441eb04cbe8 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index f24d08909b8..7f922c2eea5 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 31b5187a195..641c814d122 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b CANCEL_STOP: cancel_stop, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if not config_entry.update_listeners: config_entry.add_update_listener(async_update_options) diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index d5e4cded08d..df9dec3d9af 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -68,7 +68,9 @@ class RoonServer: ) # initialize media_player platform - hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) # Initialize Roon background polling asyncio.create_task(self.async_do_loop()) diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 8647ab38a78..79504f17d65 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -8,7 +8,7 @@ PLATFORMS = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Raspberry Pi Power Supply Checker from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 2c8c8bb108d..258a5968ab1 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENERS: [], } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index c03d27fefe5..aca8d1cd9f4 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -242,7 +242,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index fe7d8a79afd..41e0638c634 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/season/__init__.py b/homeassistant/components/season/__init__.py index 6d4a2974522..f67abee3bea 100644 --- a/homeassistant/components/season/__init__.py +++ b/homeassistant/components/season/__init__.py @@ -7,7 +7,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index e938f7132e0..82b25802b02 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -130,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def async_sense_update(_): """Retrieve latest state.""" diff --git a/homeassistant/components/senseme/__init__.py b/homeassistant/components/senseme/__init__.py index 7a64a23002f..a744dd0d0b8 100644 --- a/homeassistant/components/senseme/__init__.py +++ b/homeassistant/components/senseme/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await device.async_update(not status) hass.data[DOMAIN][entry.entry_id] = device - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index dc02e9ee686..ee3cba7d001 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 08aa26fa3c5..6f68189e8bb 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -113,7 +113,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index aad05652b0b..0c4f7bb0bfc 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/sia/__init__.py b/homeassistant/components/sia/__init__.py index dbbb12f29ce..31ae36f793d 100644 --- a/homeassistant/components/sia/__init__.py +++ b/homeassistant/components/sia/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"SIA Server at port {entry.data[CONF_PORT]} could not start." ) from exc - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index 4f46e88162c..399da14c2ad 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -141,4 +141,4 @@ class SIAHub: return hub.update_accounts() await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index c74efab61ac..14a0d6dd8e9 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -327,7 +327,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = simplisafe - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def extract_system(func: Callable) -> Callable: diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 00c7a533590..9f032327d62 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -105,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] ) hass.data[DOMAIN][entry.entry_id] = device_coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index b98be3dcfe1..f70bed1333e 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client=gateway, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/slimproto/__init__.py b/homeassistant/components/slimproto/__init__.py index 96932e1e81f..a96ff7ae925 100644 --- a/homeassistant/components/slimproto/__init__.py +++ b/homeassistant/components/slimproto/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = slimserver # initialize platform(s) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # setup event listeners async def on_hass_stop(event: Event) -> None: diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 556c2a16b6c..13f402b53c3 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -120,7 +120,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PYSMA_DEVICE_INFO: device_info, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 43c7aca9a34..c7edd46c7e2 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -105,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = SmappeeBase(hass, smappee) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index cd83a1e51c1..0fcbedca874 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: asyncio.create_task(coordinator.async_refresh()) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 3f10758076f..c45702773b2 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -183,7 +183,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index f6e36a847e5..f98dbac86a1 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await controller.async_setup_entry(entry): return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 98c8de87032..5b3f60f4b08 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -20,7 +20,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unique_id = f"{entry.data[CONF_LOCATION][CONF_LATITUDE]}-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}" hass.config_entries.async_update_entry(entry, unique_id=unique_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 0b63a3d0366..83d4bbc31f3 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: GATEWAY: gateway, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 148e684589f..dca129c7a70 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_API_CLIENT: api} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index 0ebcc3782c9..22c0e3d87d7 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = SolarlogData(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/solax/__init__.py b/homeassistant/components/solax/__init__.py index 3915f414d0e..6ede5b5df02 100644 --- a/homeassistant/components/solax/__init__.py +++ b/homeassistant/components/solax/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 034a6a0b782..1b3478b2897 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) hass.data[DOMAIN][DEVICES] = devices["shades"] - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index a7fb40eafd4..8ac6c4672fd 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 9934cc8f481..4447425f42a 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_SYSTEM_STATUS: system_status, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index 64e71af3e6a..8ab1cb18bdd 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -38,7 +38,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up songpal media player.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index e3f65d754dd..837b6c0f749 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -143,7 +143,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = SonosDiscoveryManager( hass, entry, data, hosts ) - hass.async_create_task(manager.setup_platforms_and_discovery()) + await manager.setup_platforms_and_discovery() return True @@ -377,12 +377,7 @@ class SonosDiscoveryManager: async def setup_platforms_and_discovery(self): """Set up platforms and discovery.""" - await asyncio.gather( - *( - self.hass.config_entries.async_forward_entry_setup(self.entry, platform) - for platform in PLATFORMS - ) - ) + await self.hass.config_entries.async_forward_entry_setups(self.entry, PLATFORMS) self.entry.async_on_unload( self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 2cbb5e405fb..00221c39a42 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index b3c1918aad4..d4122a91d62 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 560620beed4..7ee4e9649a5 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): raise ConfigEntryAuthFailed - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index 2917056b5d4..63f81c784be 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SQL from a config entry.""" entry.async_on_unload(entry.add_update_listener(async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index 89b3300fc5a..b3e2717d075 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -15,7 +15,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Logitech Squeezebox from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index ac9cf693c10..bed6780e523 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Unable to connect to Srp Energy: %s", str(ex)) raise ConfigEntryNotReady from ex - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 886a583ae93..2af7b4a75f4 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config_entry_id=entry.entry_id, **account.device_info(device) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def async_set_scan_interval(call: ServiceCall) -> None: """Set scan interval.""" diff --git a/homeassistant/components/steamist/__init__.py b/homeassistant/components/steamist/__init__.py index edd805e89d3..0a363f77e82 100644 --- a/homeassistant/components/steamist/__init__.py +++ b/homeassistant/components/steamist/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if discovery := await async_discover_device(hass, host): async_update_entry_from_discovery(hass, entry, discovery) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/stookalert/__init__.py b/homeassistant/components/stookalert/__init__.py index ad800c20e95..63458a2f78a 100644 --- a/homeassistant/components/stookalert/__init__.py +++ b/homeassistant/components/stookalert/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Stookalert from a config entry.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = stookalert.stookalert(entry.data[CONF_PROVINCE]) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 6e05586706f..83a5a1c1c9d 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ENTRY_VEHICLES: vehicle_info, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 75ea1306ccf..2e6125bc502 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) lock_state_service_schema = vol.Schema( { diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 1adeace7a96..102319cec93 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_id = async_add_to_device(hass, entry, entity_id) - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, (entry.options[CONF_TARGET_DOMAIN],) ) return True diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 68fbd4bd584..0a3d01f3382 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -78,7 +78,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sensor_type = entry.data[CONF_SENSOR_TYPE] - hass.config_entries.async_setup_platforms(entry, PLATFORMS_BY_TYPE[sensor_type]) + await hass.config_entries.async_forward_entry_setups( + entry, PLATFORMS_BY_TYPE[sensor_type] + ) return True diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index d4779e8e748..e0f328e9312 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -1,7 +1,6 @@ """The Switcher integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -96,24 +95,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] = SwitcherDataUpdateCoordinator(hass, entry, device) coordinator.async_setup() - async def platforms_setup_task() -> None: - # Must be ready before dispatcher is called - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) + # Must be ready before dispatcher is called + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - discovery_task = hass.data[DOMAIN].pop(DATA_DISCOVERY, None) - if discovery_task is not None: - discovered_devices = await discovery_task - for device in discovered_devices.values(): - on_device_data_callback(device) + discovery_task = hass.data[DOMAIN].pop(DATA_DISCOVERY, None) + if discovery_task is not None: + discovered_devices = await discovery_task + for device in discovered_devices.values(): + on_device_data_callback(device) - await async_start_bridge(hass, on_device_data_callback) - - hass.async_create_task(platforms_setup_task()) + await async_start_bridge(hass, on_device_data_callback) @callback async def stop_bridge(event: Event) -> None: diff --git a/homeassistant/components/syncthing/__init__.py b/homeassistant/components/syncthing/__init__.py index ac9d5d32e92..3950f908e85 100644 --- a/homeassistant/components/syncthing/__init__.py +++ b/homeassistant/components/syncthing/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: syncthing.subscribe() hass.data[DOMAIN][entry.entry_id] = syncthing - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def cancel_listen_task(_): await syncthing.unsubscribe() diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 792267791eb..988c92de593 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=printer.hostname(), ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index e6868491eae..c37540b14b7 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -121,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator_switches=coordinator_switches, ) hass.data.setdefault(DOMAIN, {})[entry.unique_id] = synology_data - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 1bee974a4c4..19bcc224a66 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -123,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL): return True diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 0029dbf8c89..7417ac49c96 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UPDATE_LISTENER: update_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py index 7388eec22df..2a5fca43ef1 100644 --- a/homeassistant/components/tailscale/__init__.py +++ b/homeassistant/components/tailscale/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index b7a15e82ea8..3db67b4c8be 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -136,7 +136,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 7e37c6ea7f2..da9d7e0d53b 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -1,7 +1,6 @@ """The Tasmota integration.""" from __future__ import annotations -import asyncio import logging from hatasmota.const import ( @@ -75,21 +74,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, mac, config, entry, tasmota_mqtt, device_registry ) - async def start_platforms() -> None: - await device_automation.async_setup_entry(hass, entry) - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) + await device_automation.async_setup_entry(hass, entry) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX] + await discovery.async_start( + hass, discovery_prefix, entry, tasmota_mqtt, async_discover_device + ) - discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX] - await discovery.async_start( - hass, discovery_prefix, entry, tasmota_mqtt, async_discover_device - ) - - hass.async_create_task(start_platforms()) return True diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index 339ec6eb895..af7939a45f5 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = TautulliDataUpdateCoordinator(hass, host_configuration, api_client) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index c4c7c551375..b1d5811c610 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_coordinator=coordinator, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/threshold/__init__.py b/homeassistant/components/threshold/__init__.py index 75f4c5d1abc..2ca1410a890 100644 --- a/homeassistant/components/threshold/__init__.py +++ b/homeassistant/components/threshold/__init__.py @@ -7,7 +7,9 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Min/Max from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + await hass.config_entries.async_forward_entry_setups( + entry, (Platform.BINARY_SENSOR,) + ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index a0fda8823c4..34f5843412e 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Failed to login. %s", exp) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 0787e00a489..cca0706954f 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -105,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_TILE: tiles, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tod/__init__.py b/homeassistant/components/tod/__init__.py index 09038836e2e..e404826534e 100644 --- a/homeassistant/components/tod/__init__.py +++ b/homeassistant/components/tod/__init__.py @@ -8,7 +8,9 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Times of the Day from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + await hass.config_entries.async_forward_entry_setups( + entry, (Platform.BINARY_SENSOR,) + ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 3b78adacbac..a75cb7ce298 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 3d00d6216a0..6af3c0e066b 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Spin up the platforms - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # If Home Assistant is already in a running state, register the webhook # immediately, else trigger it after Home Assistant has finished starting. diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 10e621e6668..50c18000baa 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from ex hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 7e103730c6d..1679965b070 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -98,7 +98,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "Traccar", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index ff2ba9c34bf..aa5048b89c0 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -102,7 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][CLIENT] = tractive hass.data[DOMAIN][entry.entry_id][TRACKABLES] = trackables - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def cancel_listen_task(_: Event) -> None: await tractive.unsubscribe() diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index e79cd8396e1..aa61be1e782 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -135,7 +135,7 @@ async def async_setup_entry( async_track_time_interval(hass, async_keep_alive, timedelta(seconds=60)) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_ferry/__init__.py b/homeassistant/components/trafikverket_ferry/__init__.py index 5042e7c5167..c522acb6d12 100644 --- a/homeassistant/components/trafikverket_ferry/__init__.py +++ b/homeassistant/components/trafikverket_ferry/__init__.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index ee026371b04..c1c756be9ed 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "train_api": train_api, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_weatherstation/__init__.py b/homeassistant/components/trafikverket_weatherstation/__init__.py index eab54f8f45b..13b88918133 100644 --- a/homeassistant/components/trafikverket_weatherstation/__init__.py +++ b/homeassistant/components/trafikverket_weatherstation/__init__.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index ae5d2dacf61..48d4a0e5163 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -168,7 +168,9 @@ class TransmissionClient: self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) def add_torrent(service: ServiceCall) -> None: """Add new torrent to download.""" diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 97422179960..9f25cfe7773 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -123,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) device_ids.add(device.id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index bbe306392ba..c4fe53c67f0 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data.setdefault(DOMAIN, {})[entry.data[CONF_ID]] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index a8347889895..3b0228e64b0 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = client hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_INFO] = device_info - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py index 587854a3a7e..eb24e5d9a78 100644 --- a/homeassistant/components/ukraine_alarm/__init__.py +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 5b4059ee1d4..30b1d1ad56d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async_setup_services(hass) hass.http.register_view(ThumbnailProxyView(hass)) hass.http.register_view(VideoProxyView(hass)) diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index e4e1900d8f2..b2980ace4e1 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb} - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) def _element_changed(element, changeset): if (change := changeset.get("last_change")) is None: diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index e42659948d8..cd356925de1 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_UPCLOUD].coordinators[entry.data[CONF_USERNAME]] = coordinator # Forward entry setup - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 27d69f9c509..a45e58f28bc 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -167,7 +167,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Setup platforms, creating sensors/binary_sensors. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/uptime/__init__.py b/homeassistant/components/uptime/__init__.py index 1c36fea3b32..b2f912751f7 100644 --- a/homeassistant/components/uptime/__init__.py +++ b/homeassistant/components/uptime/__init__.py @@ -7,7 +7,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index a4c975ff58e..14221463c0e 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 05ed01f8208..b17592cdf0b 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -200,7 +200,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.options.get(CONF_TARIFFS): # Only a single meter sensor is required hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] = None - hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) else: # Create tariff selection + one meter sensor for each tariff entity_entry = entity_registry.async_get_or_create( @@ -209,7 +209,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_UTILITY][entry.entry_id][ CONF_TARIFF_ENTITY ] = entity_entry.entity_id - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, (Platform.SELECT, Platform.SENSOR) ) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index a69542596c8..6a2234af9e6 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -219,7 +219,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "name": name, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 9b5a52306d8..eeeee2f9716 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _migrate_device_identifiers(hass, entry.entry_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.services.has_service(DOMAIN, SERVICE_SCAN): return True diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 759908c87e4..b543f6d947a 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: await venstar_data_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[config.entry_id] = venstar_data_coordinator - hass.config_entries.async_setup_platforms(config, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config, PLATFORMS) return True diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 2dda8fdb7c4..a63e63d74c0 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -142,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: set_controller_data(hass, entry, controller_data) # Forward the config data to the necessary platforms. - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, platforms=get_configured_platforms(controller_data) ) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index d42a6ad004b..9a64061554d 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/version/__init__.py b/homeassistant/components/version/__init__.py index 22dbad36d7b..878ed3d0138 100644 --- a/homeassistant/components/version/__init__.py +++ b/homeassistant/components/version/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index eb18a1229ca..10aa49514e5 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -54,22 +54,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b fans = hass.data[DOMAIN][VS_FANS] = [] lights = hass.data[DOMAIN][VS_LIGHTS] = [] sensors = hass.data[DOMAIN][VS_SENSORS] = [] + platforms = [] if device_dict[VS_SWITCHES]: switches.extend(device_dict[VS_SWITCHES]) - hass.async_create_task(forward_setup(config_entry, Platform.SWITCH)) + platforms.append(Platform.SWITCH) if device_dict[VS_FANS]: fans.extend(device_dict[VS_FANS]) - hass.async_create_task(forward_setup(config_entry, Platform.FAN)) + platforms.append(Platform.FAN) if device_dict[VS_LIGHTS]: lights.extend(device_dict[VS_LIGHTS]) - hass.async_create_task(forward_setup(config_entry, Platform.LIGHT)) + platforms.append(Platform.LIGHT) if device_dict[VS_SENSORS]: sensors.extend(device_dict[VS_SENSORS]) - hass.async_create_task(forward_setup(config_entry, Platform.SENSOR)) + platforms.append(Platform.SENSOR) + + await hass.config_entries.async_forward_entry_setups(config_entry, platforms) async def async_new_device_discovery(service: ServiceCall) -> None: """Discover if new devices should be added.""" diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 221f3a4374a..20d237ee4e4 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(setup_vicare_api, hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py index ac3b8b87f3f..82fe4c7bb70 100644 --- a/homeassistant/components/vilfo/__init__.py +++ b/homeassistant/components/vilfo/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = vilfo_router - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 1e0d5a322fb..38cf916a5e6 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_refresh() hass.data[DOMAIN][CONF_APPS] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vlc_telnet/__init__.py b/homeassistant/components/vlc_telnet/__init__.py index 9fe3b97ab31..a8a0c16be8e 100644 --- a/homeassistant/components/vlc_telnet/__init__.py +++ b/homeassistant/components/vlc_telnet/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: domain_data = hass.data.setdefault(DOMAIN, {}) domain_data[entry.entry_id] = {DATA_VLC: vlc, DATA_AVAILABLE: available} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py index 8189aeef1f4..77119b9a65e 100644 --- a/homeassistant/components/volumio/__init__.py +++ b/homeassistant/components/volumio/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_INFO: info, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vulcan/__init__.py b/homeassistant/components/vulcan/__init__.py index 50f525d9a68..8e66075935b 100644 --- a/homeassistant/components/vulcan/__init__.py +++ b/homeassistant/components/vulcan/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from err hass.data[DOMAIN][entry.entry_id] = client - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index ae003d84a9c..f0392f808ae 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -208,7 +208,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = wallbox_coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index 2f07658b923..cac73f597f6 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 3b193d6c06b..4e82af5119c 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index e759077ad3e..d9b2acb1836 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = wrapper - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 659b4602f0d..1991ec3806c 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {AUTH_INSTANCE_KEY: auth} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/whois/__init__.py b/homeassistant/components/whois/__init__.py index aa64ebecf83..746e83a2677 100644 --- a/homeassistant/components/whois/__init__.py +++ b/homeassistant/components/whois/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index f2d11d53862..0472a0bc3b3 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Port %s already in use", entry.data[CONF_PORT]) raise ConfigEntryNotReady from exc - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 2cdcf20c1ea..fefde1644ad 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = parent # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 6e8dee9a774..1b77faa7a61 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -153,7 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Start subscription check in the background, outside this component's setup. async_call_later(hass, 1, async_call_later_callback) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index b47db32d90f..396893b04b1 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -133,7 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = WizData( coordinator=coordinator, bulb=bulb, scenes=scenes ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index bcfb98b9916..809afdfb3c7 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Reload entry when its updated. entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 19cafa89a13..9d76c61806b 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -106,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][COORDINATOR] = coordinator hass.data[DOMAIN][entry.entry_id][DEVICE_ID] = device_id - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index dea1b470b9e..0b40ce84816 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -104,7 +104,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 19bbec8bbf5..bee773d2094 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -114,7 +114,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "coordinator": coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 0c9696a42ef..da3303494e5 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -196,7 +196,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: platforms = GATEWAY_PLATFORMS_NO_KEY - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) return True diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index e2dc36ff568..17211474f97 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -436,10 +436,7 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> KEY_COORDINATOR: coordinator_dict, } - for platform in GATEWAY_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + await hass.config_entries.async_forward_entry_setups(entry, GATEWAY_PLATFORMS) async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -452,7 +449,7 @@ async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> b entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) return True diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index 8712241a2aa..763742cce70 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) return True diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index a3362e0558a..b8117df056a 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.musiccast.device.enable_polling() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 4fd742e24f5..c07852629a9 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -216,7 +216,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (asyncio.TimeoutError, OSError, BulbException) as ex: raise ConfigEntryNotReady from ex - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Wait to install the reload listener until everything was successfully initialized entry.async_on_unload(entry.add_update_listener(_async_update_listener)) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 92068d1e26e..714b0b25070 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_coordinator.data = {} device_coordinators[device.device_id] = device_coordinator hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATORS] = device_coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py index 3339cdccd36..0026d2ec484 100644 --- a/homeassistant/components/youless/__init__.py +++ b/homeassistant/components/youless/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py index e8cc6962a0b..43a768c3844 100644 --- a/homeassistant/components/zerproc/__init__.py +++ b/homeassistant/components/zerproc/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if DATA_ADDRESSES not in hass.data[DOMAIN]: hass.data[DOMAIN][DATA_ADDRESSES] = set() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e7f65c38ec1..b25b62aa6e0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1158,9 +1158,24 @@ class ConfigEntries: self, entry: ConfigEntry, platforms: Iterable[Platform | str] ) -> None: """Forward the setup of an entry to platforms.""" + report( + "called async_setup_platforms instead of awaiting async_forward_entry_setups; " + "this will fail in version 2022.12", + # Raise this to warning once all core integrations have been migrated + level=logging.DEBUG, + error_if_core=False, + ) for platform in platforms: self.hass.async_create_task(self.async_forward_entry_setup(entry, platform)) + async def async_forward_entry_setups( + self, entry: ConfigEntry, platforms: Iterable[Platform | str] + ) -> None: + """Forward the setup of an entry to platforms.""" + await asyncio.gather( + *(self.async_forward_entry_setup(entry, platform) for platform in platforms) + ) + async def async_forward_entry_setup( self, entry: ConfigEntry, domain: Platform | str ) -> bool: @@ -1169,9 +1184,6 @@ class ConfigEntries: By default an entry is setup with the component it belongs to. If that component also has related platforms, the component will have to forward the entry to be setup by that component. - - You don't want to await this coroutine if it is called as part of the - setup of a component, because it can cause a deadlock. """ # Setup Component if not set up yet if domain not in self.hass.config.components: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 2599b4b3c85..8d5f9e52025 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -265,6 +265,10 @@ async def _async_setup_component( await asyncio.sleep(0) await hass.config_entries.flow.async_wait_init_flow_finish(domain) + # Add to components before the async_setup + # call to avoid a deadlock when forwarding platforms + hass.config.components.add(domain) + await asyncio.gather( *( entry.async_setup(hass, integration=integration) @@ -272,8 +276,6 @@ async def _async_setup_component( ) ) - hass.config.components.add(domain) - # Cleanup if domain in hass.data[DATA_SETUP]: hass.data[DATA_SETUP].pop(domain) From 85148b343db053a742f8a6cc296eb488319ab695 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 9 Jul 2022 10:32:15 -0600 Subject: [PATCH 242/820] Bump regenmaschine to 2022.07.1 (#74815) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 4f06ed0d71b..b318ef7f295 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.07.0"], + "requirements": ["regenmaschine==2022.07.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 3f96f8eb711..69733bda770 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2068,7 +2068,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.07.0 +regenmaschine==2022.07.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef1b8f5941e..27541bcc27e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1382,7 +1382,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.07.0 +regenmaschine==2022.07.1 # homeassistant.components.renault renault-api==0.1.11 From 124c8e8f7334386fae2ba1f77f4f826036f67fca Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Sat, 9 Jul 2022 18:39:30 +0200 Subject: [PATCH 243/820] Add sensors for HomeWizard Watermeter (#74756) Co-authored-by: Franck Nijhof --- .../components/homewizard/manifest.json | 2 +- homeassistant/components/homewizard/sensor.py | 14 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/homewizard/fixtures/data.json | 4 +- .../components/homewizard/test_diagnostics.py | 2 + tests/components/homewizard/test_sensor.py | 82 +++++++++++++++++++ 7 files changed, 104 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index e426234b449..97b3c80b50d 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/homewizard", "codeowners": ["@DCSBL"], "dependencies": [], - "requirements": ["python-homewizard-energy==1.0.3"], + "requirements": ["python-homewizard-energy==1.1.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index b7f759f6a0e..de39ce6f242 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -119,6 +119,20 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="active_liter_lpm", + name="Active Water Usage", + native_unit_of_measurement="l/min", + icon="mdi:water", + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="total_liter_m3", + name="Total Water Usage", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + icon="mdi:gauge", + state_class=SensorStateClass.TOTAL_INCREASING, + ), ) diff --git a/requirements_all.txt b/requirements_all.txt index 69733bda770..2ab6ccf679e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1907,7 +1907,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.0.3 +python-homewizard-energy==1.1.0 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27541bcc27e..727724e1e1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1281,7 +1281,7 @@ python-ecobee-api==0.2.14 python-forecastio==1.4.0 # homeassistant.components.homewizard -python-homewizard-energy==1.0.3 +python-homewizard-energy==1.1.0 # homeassistant.components.izone python-izone==1.2.3 diff --git a/tests/components/homewizard/fixtures/data.json b/tests/components/homewizard/fixtures/data.json index b6eada38038..35cfd2197a1 100644 --- a/tests/components/homewizard/fixtures/data.json +++ b/tests/components/homewizard/fixtures/data.json @@ -12,5 +12,7 @@ "active_power_l2_w": 456, "active_power_l3_w": 123.456, "total_gas_m3": 1122.333, - "gas_timestamp": 210314112233 + "gas_timestamp": 210314112233, + "active_liter_lpm": 12.345, + "total_liter_m3": 1234.567 } diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index e477c94d914..899bfb5fb2f 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -41,6 +41,8 @@ async def test_diagnostics( "active_power_l3_w": 123.456, "total_gas_m3": 1122.333, "gas_timestamp": "2021-03-14T11:22:33", + "active_liter_lpm": 12.345, + "total_liter_m3": 1234.567, }, "state": {"power_on": True, "switch_lock": False, "brightness": 255}, }, diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 8195aa11708..a81291861a2 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -534,6 +534,88 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config assert ATTR_ICON not in state.attributes +async def test_sensor_entity_active_liters( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active liters (watermeter).""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_liter_lpm": 12.345})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_water_usage") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_water_usage" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_active_liter_lpm" + assert not entry.disabled + assert state.state == "12.345" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active Water Usage" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "l/min" + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:water" + + +async def test_sensor_entity_total_liters( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads total liters (watermeter).""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"total_liter_m3": 1234.567})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_total_water_usage") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_total_water_usage" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_total_liter_m3" + assert not entry.disabled + assert state.state == "1234.567" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Total Water Usage" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:gauge" + + async def test_sensor_entity_disabled_when_null( hass, mock_config_entry_data, mock_config_entry ): From 72d134be52b5f6abf3f09e738abf4d4da1886d01 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 9 Jul 2022 19:05:49 +0200 Subject: [PATCH 244/820] Migrate devolo Home Network to new entity naming (#74741) --- .../components/devolo_home_network/entity.py | 2 ++ .../devolo_home_network/test_binary_sensor.py | 14 +++++++-- .../devolo_home_network/test_sensor.py | 29 ++++++++++++++----- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index dd26324bc2c..b7b2275109f 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -15,6 +15,8 @@ from .const import DOMAIN class DevoloEntity(CoordinatorEntity): """Representation of a devolo home network device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, device: Device, device_name: str ) -> None: diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 564e6e5ade6..8f9936be5bb 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -9,7 +9,12 @@ from homeassistant.components.devolo_home_network.const import ( CONNECTED_TO_ROUTER, LONG_UPDATE_INTERVAL, ) -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import EntityCategory @@ -25,10 +30,11 @@ from tests.common import async_fire_time_changed async def test_binary_sensor_setup(hass: HomeAssistant): """Test default setup of the binary sensor component.""" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.{CONNECTED_TO_ROUTER}") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}") is None await hass.config_entries.async_unload(entry.entry_id) @@ -36,8 +42,9 @@ async def test_binary_sensor_setup(hass: HomeAssistant): @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_attached_to_router(hass: HomeAssistant): """Test state change of a attached_to_router binary sensor device.""" - state_key = f"{DOMAIN}.{CONNECTED_TO_ROUTER}" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}" er = entity_registry.async_get(hass) @@ -47,6 +54,7 @@ async def test_update_attached_to_router(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected to router" assert er.async_get(state_key).entity_category == EntityCategory.DIAGNOSTIC diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index 582d6533802..33499f512fa 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.devolo_home_network.const import ( SHORT_UPDATE_INTERVAL, ) from homeassistant.components.sensor import DOMAIN, SensorStateClass -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import EntityCategory @@ -24,12 +24,13 @@ from tests.common import async_fire_time_changed async def test_sensor_setup(hass: HomeAssistant): """Test default setup of the sensor component.""" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.connected_wifi_clients") is not None - assert hass.states.get(f"{DOMAIN}.connected_plc_devices") is None - assert hass.states.get(f"{DOMAIN}.neighboring_wifi_networks") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_connected_wifi_clients") is not None + assert hass.states.get(f"{DOMAIN}.{device_name}_connected_plc_devices") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_neighboring_wifi_networks") is None await hass.config_entries.async_unload(entry.entry_id) @@ -37,15 +38,18 @@ async def test_sensor_setup(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") async def test_update_connected_wifi_clients(hass: HomeAssistant): """Test state change of a connected_wifi_clients sensor device.""" - state_key = f"{DOMAIN}.connected_wifi_clients" - entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_connected_wifi_clients" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected Wifi clients" + ) assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT # Emulate device failure @@ -74,8 +78,9 @@ async def test_update_connected_wifi_clients(hass: HomeAssistant): @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_neighboring_wifi_networks(hass: HomeAssistant): """Test state change of a neighboring_wifi_networks sensor device.""" - state_key = f"{DOMAIN}.neighboring_wifi_networks" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_neighboring_wifi_networks" er = entity_registry.async_get(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -83,6 +88,10 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] + == f"{entry.title} Neighboring Wifi networks" + ) assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure @@ -111,8 +120,9 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_connected_plc_devices(hass: HomeAssistant): """Test state change of a connected_plc_devices sensor device.""" - state_key = f"{DOMAIN}.connected_plc_devices" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_connected_plc_devices" er = entity_registry.async_get(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -120,6 +130,9 @@ async def test_update_connected_plc_devices(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected PLC devices" + ) assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure From 3aff5fd2e634bf178b616b552c763ef470d7a59b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 19:08:53 +0200 Subject: [PATCH 245/820] Migrate Open-Meteo to new entity naming style (#74695) --- homeassistant/components/open_meteo/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index d1f0adf2e87..6af9900ec15 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -37,6 +37,7 @@ class OpenMeteoWeatherEntity( ): """Defines an Open-Meteo weather entity.""" + _attr_has_entity_name = True _attr_native_precipitation_unit = LENGTH_MILLIMETERS _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR @@ -50,7 +51,6 @@ class OpenMeteoWeatherEntity( """Initialize Open-Meteo weather entity.""" super().__init__(coordinator=coordinator) self._attr_unique_id = entry.entry_id - self._attr_name = entry.title self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, From 1cf8b761247139f46bc5ddb10e34b1fb24965133 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 19:11:14 +0200 Subject: [PATCH 246/820] Migrate CO2 Signal to new entity naming style (#74696) --- .../components/co2signal/__init__.py | 5 --- homeassistant/components/co2signal/sensor.py | 40 +++++++++---------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index cc7738741bb..a5bae23332d 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -15,7 +15,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_COUNTRY_CODE, DOMAIN -from .util import get_extra_name PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -73,10 +72,6 @@ class CO2SignalCoordinator(DataUpdateCoordinator[CO2SignalResponse]): """Return entry ID.""" return self._entry.entry_id - def get_extra_name(self) -> str | None: - """Return the extra name describing the location if not home.""" - return get_extra_name(self._entry.data) - async def _async_update_data(self) -> CO2SignalResponse: """Fetch the latest data from the source.""" try: diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 23303474e3b..f7514664698 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -5,14 +5,17 @@ from dataclasses import dataclass from datetime import timedelta from typing import cast -from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import CO2SignalCoordinator @@ -22,12 +25,9 @@ SCAN_INTERVAL = timedelta(minutes=3) @dataclass -class CO2SensorEntityDescription: +class CO2SensorEntityDescription(SensorEntityDescription): """Provide a description of a CO2 sensor.""" - key: str - name: str - unit_of_measurement: str | None = None # For backwards compat, allow description to override unique ID key to use unique_id: str | None = None @@ -42,7 +42,7 @@ SENSORS = ( CO2SensorEntityDescription( key="fossilFuelPercentage", name="Grid fossil fuel percentage", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), ) @@ -58,21 +58,18 @@ async def async_setup_entry( class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): """Implementation of the CO2Signal sensor.""" - _attr_state_class = SensorStateClass.MEASUREMENT + entity_description: CO2SensorEntityDescription + _attr_has_entity_name = True _attr_icon = "mdi:molecule-co2" + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, coordinator: CO2SignalCoordinator, description: CO2SensorEntityDescription ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._description = description + self.entity_description = description - name = description.name - if extra_name := coordinator.get_extra_name(): - name = f"{extra_name} - {name}" - - self._attr_name = name self._attr_extra_state_attributes = { "country_code": coordinator.data["countryCode"], ATTR_ATTRIBUTION: ATTRIBUTION, @@ -92,19 +89,22 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): def available(self) -> bool: """Return True if entity is available.""" return ( - super().available and self._description.key in self.coordinator.data["data"] + super().available + and self.entity_description.key in self.coordinator.data["data"] ) @property - def native_value(self) -> StateType: + def native_value(self) -> float | None: """Return sensor state.""" - if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[literal-required] + if (value := self.coordinator.data["data"][self.entity_description.key]) is None: # type: ignore[literal-required] return None return round(value, 2) @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" - if self._description.unit_of_measurement: - return self._description.unit_of_measurement - return cast(str, self.coordinator.data["units"].get(self._description.key)) + if self.entity_description.unit_of_measurement: + return self.entity_description.unit_of_measurement + return cast( + str, self.coordinator.data["units"].get(self.entity_description.key) + ) From d208bd461ddc07fbdb2c0199a4f75a04a173cb1c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 19:12:19 +0200 Subject: [PATCH 247/820] Migrate Renault to new entity naming style (#74709) --- .../components/renault/binary_sensor.py | 4 +-- homeassistant/components/renault/button.py | 4 +-- .../components/renault/renault_entities.py | 10 +----- homeassistant/components/renault/select.py | 2 +- homeassistant/components/renault/sensor.py | 36 +++++++++---------- 5 files changed, 24 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index a24c9be4e6d..f309a8f188a 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -85,7 +85,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( key="plugged_in", coordinator="battery", device_class=BinarySensorDeviceClass.PLUG, - name="Plugged In", + name="Plugged in", on_key="plugStatus", on_value=PlugState.PLUGGED.value, ), @@ -130,7 +130,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( coordinator="lock_status", # On means open, Off means closed device_class=BinarySensorDeviceClass.DOOR, - name=f"{door} Door", + name=f"{door.capitalize()} door", on_key=f"doorStatus{door.replace(' ','')}", on_value="open", ) diff --git a/homeassistant/components/renault/button.py b/homeassistant/components/renault/button.py index e62bdf083ae..71d197ac335 100644 --- a/homeassistant/components/renault/button.py +++ b/homeassistant/components/renault/button.py @@ -71,13 +71,13 @@ BUTTON_TYPES: tuple[RenaultButtonEntityDescription, ...] = ( async_press=_start_air_conditioner, key="start_air_conditioner", icon="mdi:air-conditioner", - name="Start Air Conditioner", + name="Start air conditioner", ), RenaultButtonEntityDescription( async_press=_start_charge, key="start_charge", icon="mdi:ev-station", - name="Start Charge", + name="Start charge", requires_electricity=True, ), ) diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index 82f071decae..188d429016a 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from typing import cast -from homeassistant.const import ATTR_NAME from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -28,6 +27,7 @@ class RenaultDataEntityDescription(EntityDescription, RenaultDataRequiredKeysMix class RenaultEntity(Entity): """Implementation of a Renault entity with a data coordinator.""" + _attr_has_entity_name = True entity_description: EntityDescription def __init__( @@ -41,14 +41,6 @@ class RenaultEntity(Entity): self._attr_device_info = self.vehicle.device_info self._attr_unique_id = f"{self.vehicle.details.vin}_{description.key}".lower() - @property - def name(self) -> str: - """Return the name of the entity. - - Overridden to include the device name. - """ - return f"{self.vehicle.device_info[ATTR_NAME]} {self.entity_description.name}" - class RenaultDataEntity( CoordinatorEntity[RenaultDataUpdateCoordinator[T]], RenaultEntity diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index e7ec97b3927..9af47206e3c 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -98,7 +98,7 @@ SENSOR_TYPES: tuple[RenaultSelectEntityDescription, ...] = ( data_key="chargeMode", device_class=DEVICE_CLASS_CHARGE_MODE, icon_lambda=_get_charge_mode_icon, - name="Charge Mode", + name="Charge mode", options=["always", "always_charging", "schedule_mode"], ), ) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 3b486af175b..7e692182ff9 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -164,7 +164,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryLevel", device_class=SensorDeviceClass.BATTERY, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Battery Level", + name="Battery level", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), @@ -175,7 +175,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=DEVICE_CLASS_CHARGE_STATE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon_lambda=_get_charge_state_icon, - name="Charge State", + name="Charge state", value_lambda=_get_charge_state_formatted, ), RenaultSensorEntityDescription( @@ -184,7 +184,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingRemainingTime", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon="mdi:timer", - name="Charging Remaining Time", + name="Charging remaining time", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), @@ -195,7 +195,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingInstantaneousPower", device_class=SensorDeviceClass.CURRENT, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Charging Power", + name="Charging power", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, ), @@ -206,7 +206,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingInstantaneousPower", device_class=SensorDeviceClass.POWER, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Charging Power", + name="Charging power", native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, value_lambda=_get_charging_power, @@ -218,7 +218,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=DEVICE_CLASS_PLUG_STATE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon_lambda=_get_plug_state_icon, - name="Plug State", + name="Plug state", value_lambda=_get_plug_state_formatted, ), RenaultSensorEntityDescription( @@ -227,7 +227,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryAutonomy", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon="mdi:ev-station", - name="Battery Autonomy", + name="Battery autonomy", native_unit_of_measurement=LENGTH_KILOMETERS, state_class=SensorStateClass.MEASUREMENT, ), @@ -237,7 +237,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryAvailableEnergy", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], device_class=SensorDeviceClass.ENERGY, - name="Battery Available Energy", + name="Battery available energy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.MEASUREMENT, ), @@ -247,7 +247,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryTemperature", device_class=SensorDeviceClass.TEMPERATURE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Battery Temperature", + name="Battery temperature", native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), @@ -258,7 +258,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="timestamp", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], entity_registry_enabled_default=False, - name="Battery Last Activity", + name="Battery last activity", value_lambda=_get_utc_value, ), RenaultSensorEntityDescription( @@ -278,7 +278,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="fuelAutonomy", entity_class=RenaultSensor[KamereonVehicleCockpitData], icon="mdi:gas-station", - name="Fuel Autonomy", + name="Fuel autonomy", native_unit_of_measurement=LENGTH_KILOMETERS, state_class=SensorStateClass.MEASUREMENT, requires_fuel=True, @@ -290,7 +290,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="fuelQuantity", entity_class=RenaultSensor[KamereonVehicleCockpitData], icon="mdi:fuel", - name="Fuel Quantity", + name="Fuel quantity", native_unit_of_measurement=VOLUME_LITERS, state_class=SensorStateClass.MEASUREMENT, requires_fuel=True, @@ -302,7 +302,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=SensorDeviceClass.TEMPERATURE, data_key="externalTemperature", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], - name="Outside Temperature", + name="Outside temperature", native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), @@ -311,7 +311,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( coordinator="hvac_status", data_key="socThreshold", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], - name="HVAC SOC Threshold", + name="HVAC SoC threshold", native_unit_of_measurement=PERCENTAGE, ), RenaultSensorEntityDescription( @@ -321,7 +321,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="lastUpdateTime", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], entity_registry_enabled_default=False, - name="HVAC Last Activity", + name="HVAC last activity", value_lambda=_get_utc_value, ), RenaultSensorEntityDescription( @@ -331,7 +331,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="lastUpdateTime", entity_class=RenaultSensor[KamereonVehicleLocationData], entity_registry_enabled_default=False, - name="Location Last Activity", + name="Location last activity", value_lambda=_get_utc_value, ), RenaultSensorEntityDescription( @@ -339,7 +339,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( coordinator="res_state", data_key="details", entity_class=RenaultSensor[KamereonVehicleResStateData], - name="Remote Engine Start", + name="Remote engine start", ), RenaultSensorEntityDescription( key="res_state_code", @@ -347,6 +347,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="code", entity_class=RenaultSensor[KamereonVehicleResStateData], entity_registry_enabled_default=False, - name="Remote Engine Start Code", + name="Remote engine start code", ), ) From 04bb2d1e5d4c3d0b97e5d260fc9ddcdb9a59b8b7 Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Sat, 9 Jul 2022 19:13:46 +0200 Subject: [PATCH 248/820] Update pyCEC to version 0.5.2 (#74742) --- homeassistant/components/hdmi_cec/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index ff2411db35a..8ea56a51fa9 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -2,7 +2,7 @@ "domain": "hdmi_cec", "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", - "requirements": ["pyCEC==0.5.1"], + "requirements": ["pyCEC==0.5.2"], "codeowners": [], "iot_class": "local_push", "loggers": ["pycec"] diff --git a/requirements_all.txt b/requirements_all.txt index 2ab6ccf679e..fad4f243350 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1326,7 +1326,7 @@ py-zabbix==1.1.7 py17track==2021.12.2 # homeassistant.components.hdmi_cec -pyCEC==0.5.1 +pyCEC==0.5.2 # homeassistant.components.control4 pyControl4==0.0.6 From 24ca656372f907f43393374f8fe9ccc57c5340b4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 19:17:50 +0200 Subject: [PATCH 249/820] Migrate deCONZ Group and Scenes to new entity naming style (#74761) --- homeassistant/components/deconz/deconz_device.py | 16 +++++++++++++--- homeassistant/components/deconz/light.py | 4 ++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 94051cbf0ac..e9be4658fb5 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -120,6 +120,8 @@ class DeconzDevice(DeconzBase, Entity): class DeconzSceneMixin(DeconzDevice): """Representation of a deCONZ scene.""" + _attr_has_entity_name = True + _device: PydeconzScene def __init__( @@ -130,7 +132,9 @@ class DeconzSceneMixin(DeconzDevice): """Set up a scene.""" super().__init__(device, gateway) - self._attr_name = device.full_name + self.group = self.gateway.api.groups[device.group_id] + + self._attr_name = device.name self._group_identifier = self.get_parent_identifier() def get_device_identifier(self) -> str: @@ -139,7 +143,7 @@ class DeconzSceneMixin(DeconzDevice): def get_parent_identifier(self) -> str: """Describe a unique identifier for group this scene belongs to.""" - return f"{self.gateway.bridgeid}-{self._device.group_deconz_id}" + return f"{self.gateway.bridgeid}-{self.group.deconz_id}" @property def unique_id(self) -> str: @@ -149,4 +153,10 @@ class DeconzSceneMixin(DeconzDevice): @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return DeviceInfo(identifiers={(DECONZ_DOMAIN, self._group_identifier)}) + return DeviceInfo( + identifiers={(DECONZ_DOMAIN, self._group_identifier)}, + manufacturer="Dresden Elektronik", + model="deCONZ group", + name=self.group.name, + via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 7f3a47d719d..e73314aa477 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -289,6 +289,8 @@ class DeconzLight(DeconzBaseLight[Light]): class DeconzGroup(DeconzBaseLight[Group]): """Representation of a deCONZ group.""" + _attr_has_entity_name = True + _device: Group def __init__(self, device: Group, gateway: DeconzGateway) -> None: @@ -296,6 +298,8 @@ class DeconzGroup(DeconzBaseLight[Group]): self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}" super().__init__(device, gateway) + self._attr_name = None + @property def unique_id(self) -> str: """Return a unique identifier for this device.""" From 1c4fee65c0f1e6e2c1ed1d03ec9e3303b870ad1e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 9 Jul 2022 10:21:53 -0700 Subject: [PATCH 250/820] Migrate nest to new entity naming style (#74724) --- homeassistant/components/nest/camera_sdm.py | 7 ++----- homeassistant/components/nest/climate_sdm.py | 6 +----- homeassistant/components/nest/sensor_sdm.py | 13 +++---------- tests/components/nest/test_camera_sdm.py | 3 ++- tests/components/nest/test_events.py | 1 - tests/components/nest/test_sensor_sdm.py | 7 ++++--- 6 files changed, 12 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 4e38338aee8..c26b216b21f 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -61,6 +61,8 @@ async def async_setup_sdm_entry( class NestCamera(Camera): """Devices that support cameras.""" + _attr_has_entity_name = True + def __init__(self, device: Device) -> None: """Initialize the camera.""" super().__init__() @@ -83,11 +85,6 @@ class NestCamera(Camera): # The API "name" field is a unique device identifier. return f"{self._device.name}-camera" - @property - def name(self) -> str | None: - """Return the name of the camera.""" - return self._device_info.device_name - @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 452c30073da..c13370776ad 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -97,6 +97,7 @@ class ThermostatEntity(ClimateEntity): _attr_min_temp = MIN_TEMP _attr_max_temp = MAX_TEMP + _attr_has_entity_name = True def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" @@ -115,11 +116,6 @@ class ThermostatEntity(ClimateEntity): # The API "name" field is a unique device identifier. return self._device.name - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._device_info.device_name - @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index c6d1c8b2b30..11edc9f3506 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -53,6 +53,7 @@ class SensorBase(SensorEntity): _attr_should_poll = False _attr_state_class = SensorStateClass.MEASUREMENT + _attr_has_entity_name = True def __init__(self, device: Device) -> None: """Initialize the sensor.""" @@ -73,11 +74,7 @@ class TemperatureSensor(SensorBase): _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device_info.device_name} Temperature" + _attr_name = "Temperature" @property def native_value(self) -> float: @@ -94,11 +91,7 @@ class HumiditySensor(SensorBase): _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device_info.device_name} Humidity" + _attr_name = "Humidity" @property def native_value(self) -> int: diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index 5c4194f46f6..86f05c613ed 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -17,6 +17,7 @@ from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING, StreamType from homeassistant.components.nest.const import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -210,11 +211,11 @@ async def test_camera_device( camera = hass.states.get("camera.my_camera") assert camera is not None assert camera.state == STATE_STREAMING + assert camera.attributes.get(ATTR_FRIENDLY_NAME) == "My Camera" registry = er.async_get(hass) entry = registry.async_get("camera.my_camera") assert entry.unique_id == f"{DEVICE_ID}-camera" - assert entry.original_name == "My Camera" assert entry.domain == "camera" device_registry = dr.async_get(hass) diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 83845586764..1fffcc9042b 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -160,7 +160,6 @@ async def test_event( entry = registry.async_get("camera.front") assert entry is not None assert entry.unique_id == "some-device-id-camera" - assert entry.original_name == "Front" assert entry.domain == "camera" device_registry = dr.async_get(hass) diff --git a/tests/components/nest/test_sensor_sdm.py b/tests/components/nest/test_sensor_sdm.py index bb49f13b3eb..0f98d0c05b4 100644 --- a/tests/components/nest/test_sensor_sdm.py +++ b/tests/components/nest/test_sensor_sdm.py @@ -13,6 +13,7 @@ import pytest from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -59,6 +60,7 @@ async def test_thermostat_device( assert temperature.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert temperature.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE assert temperature.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert temperature.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Temperature" humidity = hass.states.get("sensor.my_sensor_humidity") assert humidity is not None @@ -66,16 +68,15 @@ async def test_thermostat_device( assert humidity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert humidity.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY assert humidity.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert humidity.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Humidity" registry = er.async_get(hass) entry = registry.async_get("sensor.my_sensor_temperature") assert entry.unique_id == f"{DEVICE_ID}-temperature" - assert entry.original_name == "My Sensor Temperature" assert entry.domain == "sensor" entry = registry.async_get("sensor.my_sensor_humidity") assert entry.unique_id == f"{DEVICE_ID}-humidity" - assert entry.original_name == "My Sensor Humidity" assert entry.domain == "sensor" device_registry = dr.async_get(hass) @@ -195,11 +196,11 @@ async def test_device_with_unknown_type( temperature = hass.states.get("sensor.my_sensor_temperature") assert temperature is not None assert temperature.state == "25.1" + assert temperature.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Temperature" registry = er.async_get(hass) entry = registry.async_get("sensor.my_sensor_temperature") assert entry.unique_id == f"{DEVICE_ID}-temperature" - assert entry.original_name == "My Sensor Temperature" assert entry.domain == "sensor" device_registry = dr.async_get(hass) From cdcc73a414a9f2e66589c838ebc27baaedb5e728 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 19:22:23 +0200 Subject: [PATCH 251/820] Migrate Stookalert to new entity naming style (#74693) --- homeassistant/components/stookalert/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index 974635e2efd..70a25c2bfdf 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -35,15 +35,15 @@ class StookalertBinarySensor(BinarySensorEntity): _attr_attribution = "Data provided by rivm.nl" _attr_device_class = BinarySensorDeviceClass.SAFETY + _attr_has_entity_name = True def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: """Initialize a Stookalert device.""" self._client = client - self._attr_name = f"Stookalert {entry.data[CONF_PROVINCE]}" self._attr_unique_id = entry.unique_id self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{entry.entry_id}")}, - name=entry.data[CONF_PROVINCE], + name=f"Stookalert {entry.data[CONF_PROVINCE]}", manufacturer="RIVM", model="Stookalert", entry_type=DeviceEntryType.SERVICE, From 4080d2b0dabeae322f42b34b8eb553f5d2a300b9 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 9 Jul 2022 19:34:51 +0200 Subject: [PATCH 252/820] Convert philips_js to entity naming (#74721) Co-authored-by: Franck Nijhof --- homeassistant/components/philips_js/light.py | 4 +++- homeassistant/components/philips_js/media_player.py | 2 +- homeassistant/components/philips_js/remote.py | 4 +++- homeassistant/components/philips_js/switch.py | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index ab4f04a8ddc..4fa3b066214 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -137,6 +137,8 @@ class PhilipsTVLightEntity( ): """Representation of a Philips TV exposing the JointSpace API.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -151,7 +153,7 @@ class PhilipsTVLightEntity( self._attr_supported_color_modes = {ColorMode.HS, ColorMode.ONOFF} self._attr_supported_features = LightEntityFeature.EFFECT - self._attr_name = f"{coordinator.system['name']} Ambilight" + self._attr_name = "Ambilight" self._attr_unique_id = coordinator.unique_id self._attr_icon = "mdi:television-ambient-light" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 3d63865fe31..8d864436ac5 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -73,6 +73,7 @@ class PhilipsTVMediaPlayer( """Representation of a Philips TV exposing the JointSpace API.""" _attr_device_class = MediaPlayerDeviceClass.TV + _attr_has_entity_name = True def __init__( self, @@ -83,7 +84,6 @@ class PhilipsTVMediaPlayer( self._sources: dict[str, str] = {} self._supports = SUPPORT_PHILIPS_JS self._system = coordinator.system - self._attr_name = coordinator.system["name"] self._attr_unique_id = coordinator.unique_id self._attr_device_info = DeviceInfo( identifiers={ diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 38851964427..7e8c5448cca 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -30,6 +30,8 @@ async def async_setup_entry( class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteEntity): """Device that sends commands.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -37,7 +39,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE """Initialize the Philips TV.""" super().__init__(coordinator) self._tv = coordinator.api - self._attr_name = f"{coordinator.system['name']} Remote" + self._attr_name = "Remote" self._attr_unique_id = coordinator.unique_id self._attr_device_info = DeviceInfo( identifiers={ diff --git a/homeassistant/components/philips_js/switch.py b/homeassistant/components/philips_js/switch.py index bf1e83b0ec1..b66fd3296d9 100644 --- a/homeassistant/components/philips_js/switch.py +++ b/homeassistant/components/philips_js/switch.py @@ -38,6 +38,8 @@ class PhilipsTVScreenSwitch( ): """A Philips TV screen state switch.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -46,7 +48,7 @@ class PhilipsTVScreenSwitch( super().__init__(coordinator) - self._attr_name = f"{coordinator.system['name']} Screen State" + self._attr_name = "Screen state" self._attr_icon = "mdi:television-shimmer" self._attr_unique_id = f"{coordinator.unique_id}_screenstate" self._attr_device_info = DeviceInfo( From 8ce897f46281d6daee628c00007228d349f0eabe Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 9 Jul 2022 19:46:53 +0200 Subject: [PATCH 253/820] Convert fjaraskupan to entity naming (#74723) --- homeassistant/components/fjaraskupan/binary_sensor.py | 6 +++--- homeassistant/components/fjaraskupan/fan.py | 2 +- homeassistant/components/fjaraskupan/light.py | 3 ++- homeassistant/components/fjaraskupan/number.py | 4 +++- homeassistant/components/fjaraskupan/sensor.py | 4 +++- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fjaraskupan/binary_sensor.py b/homeassistant/components/fjaraskupan/binary_sensor.py index ef4f64c5ecd..0ea5c1669db 100644 --- a/homeassistant/components/fjaraskupan/binary_sensor.py +++ b/homeassistant/components/fjaraskupan/binary_sensor.py @@ -30,13 +30,13 @@ class EntityDescription(BinarySensorEntityDescription): SENSORS = ( EntityDescription( key="grease-filter", - name="Grease Filter", + name="Grease filter", device_class=BinarySensorDeviceClass.PROBLEM, is_on=lambda state: state.grease_filter_full, ), EntityDescription( key="carbon-filter", - name="Carbon Filter", + name="Carbon filter", device_class=BinarySensorDeviceClass.PROBLEM, is_on=lambda state: state.carbon_filter_full, ), @@ -68,6 +68,7 @@ class BinarySensor(CoordinatorEntity[Coordinator], BinarySensorEntity): """Grease filter sensor.""" entity_description: EntityDescription + _attr_has_entity_name = True def __init__( self, @@ -82,7 +83,6 @@ class BinarySensor(CoordinatorEntity[Coordinator], BinarySensorEntity): self._attr_unique_id = f"{device.address}-{entity_description.key}" self._attr_device_info = device_info - self._attr_name = f"{device_info['name']} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index e372d540f54..3642438d5d6 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -67,6 +67,7 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): """Fan entity.""" _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + _attr_has_entity_name = True def __init__( self, @@ -78,7 +79,6 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): super().__init__(coordinator) self._device = device self._default_on_speed = 25 - self._attr_name = device_info["name"] self._attr_unique_id = device.address self._attr_device_info = device_info self._percentage = 0 diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index 9d52c5cac82..f492f628a3a 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -29,6 +29,8 @@ async def async_setup_entry( class Light(CoordinatorEntity[Coordinator], LightEntity): """Light device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: Coordinator, @@ -42,7 +44,6 @@ class Light(CoordinatorEntity[Coordinator], LightEntity): self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_unique_id = device.address self._attr_device_info = device_info - self._attr_name = device_info["name"] async def async_turn_on(self, **kwargs): """Turn the light on.""" diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index 511d97cbed8..fb793d2328e 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -34,6 +34,8 @@ async def async_setup_entry( class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): """Periodic Venting.""" + _attr_has_entity_name = True + _attr_native_max_value: float = 59 _attr_native_min_value: float = 0 _attr_native_step: float = 1 @@ -51,7 +53,7 @@ class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): self._device = device self._attr_unique_id = f"{device.address}-periodic-venting" self._attr_device_info = device_info - self._attr_name = f"{device_info['name']} Periodic Venting" + self._attr_name = "Periodic venting" @property def native_value(self) -> float | None: diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index e4dbffab38e..81b499661b1 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -35,6 +35,8 @@ async def async_setup_entry( class RssiSensor(CoordinatorEntity[Coordinator], SensorEntity): """Sensor device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: Coordinator, @@ -45,7 +47,7 @@ class RssiSensor(CoordinatorEntity[Coordinator], SensorEntity): super().__init__(coordinator) self._attr_unique_id = f"{device.address}-signal-strength" self._attr_device_info = device_info - self._attr_name = f"{device_info['name']} Signal Strength" + self._attr_name = "Signal strength" self._attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT From 5defe67269a679cad7858009a45c75ee9b7907af Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Sat, 9 Jul 2022 10:51:47 -0700 Subject: [PATCH 254/820] `air_quality` and `filter_life` fixes for Pur131S (#74740) --- homeassistant/components/vesync/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index 2da6d8ea6b7..45018c79ca0 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -83,13 +83,12 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda device: device.details["filter_life"], + value_fn=lambda device: device.filter_life, exists_fn=lambda device: sku_supported(device, FILTER_LIFE_SUPPORTED), ), VeSyncSensorEntityDescription( key="air-quality", name="Air Quality", - state_class=SensorStateClass.MEASUREMENT, value_fn=lambda device: device.details["air_quality"], exists_fn=lambda device: sku_supported(device, AIR_QUALITY_SUPPORTED), ), From 0f33c08dca80fc39eb93e9b7ef0db5dabe22a41a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 19:52:26 +0200 Subject: [PATCH 255/820] Remove telegram_bot from mypy ignore list (#74661) --- homeassistant/components/telegram_bot/polling.py | 5 +++-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 4c0de6ade1e..762ba08ab26 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -22,10 +22,11 @@ async def async_setup_platform(hass, bot, config): return True -def process_error(update: Update, context: CallbackContext): +def process_error(update: Update, context: CallbackContext) -> None: """Telegram bot error handler.""" try: - raise context.error + if context.error: + raise context.error except (TimedOut, NetworkError, RetryAfter): # Long polling timeout or connection problem. Nothing serious. pass diff --git a/mypy.ini b/mypy.ini index a883007de0d..57544f298ab 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2819,9 +2819,6 @@ ignore_errors = true [mypy-homeassistant.components.sonos.statistics] ignore_errors = true -[mypy-homeassistant.components.telegram_bot.polling] -ignore_errors = true - [mypy-homeassistant.components.template.number] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 4e11028d574..74a921c8351 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -74,7 +74,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.sensor", "homeassistant.components.sonos.speaker", "homeassistant.components.sonos.statistics", - "homeassistant.components.telegram_bot.polling", "homeassistant.components.template.number", "homeassistant.components.template.sensor", "homeassistant.components.toon", From e74f7d13ab26563085b56970bdccf03d2fb191b1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 9 Jul 2022 18:55:33 +0100 Subject: [PATCH 256/820] Update systembridgeconnector to 3.3.2 (#74701) --- homeassistant/components/system_bridge/coordinator.py | 7 ++++--- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 6088967aa33..1719d951cf0 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -93,7 +93,7 @@ class SystemBridgeDataUpdateCoordinator( if not self.websocket_client.connected: await self._setup_websocket() - await self.websocket_client.get_data(modules) + self.hass.async_create_task(self.websocket_client.get_data(modules)) async def async_handle_module( self, @@ -107,9 +107,7 @@ class SystemBridgeDataUpdateCoordinator( async def _listen_for_data(self) -> None: """Listen for events from the WebSocket.""" - try: - await self.websocket_client.register_data_listener(MODULES) await self.websocket_client.listen(callback=self.async_handle_module) except AuthenticationException as exception: self.last_update_success = False @@ -175,6 +173,9 @@ class SystemBridgeDataUpdateCoordinator( self.async_update_listeners() self.hass.async_create_task(self._listen_for_data()) + + await self.websocket_client.register_data_listener(MODULES) + self.last_update_success = True self.async_update_listeners() diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 087613413d8..4fb2201e2c7 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridgeconnector==3.1.5"], + "requirements": ["systembridgeconnector==3.3.2"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._tcp.local."], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index fad4f243350..4cdf5deb9d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2271,7 +2271,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.1.5 +systembridgeconnector==3.3.2 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 727724e1e1d..732bf76c8df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1519,7 +1519,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridgeconnector==3.1.5 +systembridgeconnector==3.3.2 # homeassistant.components.tailscale tailscale==0.2.0 From 5360e56d09b87870f06100cbabb6a663faa85f5f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 19:59:11 +0200 Subject: [PATCH 257/820] Remove xiaomi_miio from mypy ignore list (#74669) --- .../components/xiaomi_miio/__init__.py | 16 ++++++---- .../components/xiaomi_miio/air_quality.py | 3 +- .../components/xiaomi_miio/binary_sensor.py | 4 +-- .../components/xiaomi_miio/device.py | 4 +-- .../components/xiaomi_miio/device_tracker.py | 4 +-- homeassistant/components/xiaomi_miio/fan.py | 5 ++-- .../components/xiaomi_miio/humidifier.py | 6 +++- homeassistant/components/xiaomi_miio/light.py | 23 ++++++++++---- .../components/xiaomi_miio/sensor.py | 7 +++-- .../components/xiaomi_miio/switch.py | 20 +++++++++---- mypy.ini | 30 ------------------- script/hassfest/mypy_config.py | 10 ------- 12 files changed, 63 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 17211474f97..9f0be1da528 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -18,6 +18,7 @@ from miio import ( CleaningDetails, CleaningSummary, ConsumableStatus, + Device as MiioDevice, DeviceException, DNDStatus, Fan, @@ -283,10 +284,10 @@ async def async_create_miio_device_and_coordinator( host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] name = entry.title - device = None + device: MiioDevice | None = None migrate = False update_method = _async_update_data_default - coordinator_class = DataUpdateCoordinator + coordinator_class: type[DataUpdateCoordinator] = DataUpdateCoordinator if ( model not in MODELS_HUMIDIFIER @@ -345,10 +346,13 @@ async def async_create_miio_device_and_coordinator( if migrate: # Removing fan platform entity for humidifiers and migrate the name to the config entry for migration entity_registry = er.async_get(hass) + assert entry.unique_id entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) if entity_id: # This check is entities that have a platform migration only and should be removed in the future - if migrate_entity_name := entity_registry.async_get(entity_id).name: + if (entity := entity_registry.async_get(entity_id)) and ( + migrate_entity_name := entity.name + ): hass.config_entries.async_update_entry(entry, title=migrate_entity_name) entity_registry.async_remove(entity_id) @@ -377,8 +381,10 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> name = entry.title gateway_id = entry.unique_id + assert gateway_id + # For backwards compat - if entry.unique_id.endswith("-gateway"): + if gateway_id.endswith("-gateway"): hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -419,7 +425,7 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> return async_update_data - coordinator_dict = {} + coordinator_dict: dict[str, DataUpdateCoordinator] = {} for sub_device in gateway.gateway_device.devices.values(): # Create update coordinator coordinator_dict[sub_device.sid] = DataUpdateCoordinator( diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 10d6c6129d3..30fcaa5152a 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" +from collections.abc import Callable import logging from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException @@ -218,7 +219,7 @@ class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity): return self._particulate_matter_10 -DEVICE_MAP = { +DEVICE_MAP: dict[str, dict[str, Callable]] = { MODEL_AIRQUALITYMONITOR_S1: { "device_class": AirQualityMonitor, "entity_class": AirMonitorS1, diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index d2f00e9805c..a1e833f42de 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Xiaomi Miio binary sensors.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable from dataclasses import dataclass import logging @@ -180,7 +180,7 @@ async def async_setup_entry( if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: model = config_entry.data[CONF_MODEL] - sensors = [] + sensors: Iterable[str] = [] if model in MODEL_AIRFRESH_A1 or model in MODEL_AIRFRESH_T2017: sensors = AIRFRESH_A1_BINARY_SENSORS elif model in MODEL_FAN_ZA5: diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index dc1cf86f7df..f8b5e6bc45a 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -184,9 +184,9 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): return int(timedelta.total_seconds()) @staticmethod - def _parse_datetime_time(time: datetime.time) -> str: + def _parse_datetime_time(initial_time: datetime.time) -> str: time = datetime.datetime.now().replace( - hour=time.hour, minute=time.minute, second=0, microsecond=0 + hour=initial_time.hour, minute=initial_time.minute, second=0, microsecond=0 ) if time < datetime.datetime.now(): diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index 3f819d7ab7d..1ec914fc647 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_TOKEN @@ -18,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index ac85955b347..8ce93933022 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -183,7 +183,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fan from a config entry.""" - entities = [] + entities: list[FanEntity] = [] + entity: FanEntity if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return @@ -299,7 +300,7 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): return self._preset_modes @property - def percentage(self) -> None: + def percentage(self) -> int | None: """Return the percentage based speed of the fan.""" return None diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index d5c829754d6..05c1eaf35bf 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -68,7 +68,8 @@ async def async_setup_entry( if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return - entities = [] + entities: list[HumidifierEntity] = [] + entity: HumidifierEntity model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -112,6 +113,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): _attr_device_class = HumidifierDeviceClass.HUMIDIFIER _attr_supported_features = HumidifierEntityFeature.MODES + supported_features: int def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" @@ -169,6 +171,8 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): """Representation of a Xiaomi Air Humidifier.""" + available_modes: list[str] + def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id, coordinator) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 28feb23d93e..fd6af0d9560 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -1,4 +1,6 @@ """Support for Xiaomi Philips Lights.""" +from __future__ import annotations + import asyncio import datetime from datetime import timedelta @@ -6,7 +8,14 @@ from functools import partial import logging from math import ceil -from miio import Ceil, DeviceException, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight +from miio import ( + Ceil, + Device as MiioDevice, + DeviceException, + PhilipsBulb, + PhilipsEyecare, + PhilipsMoonlight, +) from miio.gateway.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, @@ -115,7 +124,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi light from a config entry.""" - entities = [] + entities: list[LightEntity] = [] + entity: LightEntity + light: MiioDevice if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] @@ -195,7 +206,7 @@ async def async_setup_entry( async def async_service_handler(service: ServiceCall) -> None: """Map services to methods on Xiaomi Philips Lights.""" - method = SERVICE_TO_METHOD.get(service.service) + method = SERVICE_TO_METHOD[service.service] params = { key: value for key, value in service.data.items() @@ -372,7 +383,7 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): @staticmethod def delayed_turn_off_timestamp( - countdown: int, current: datetime, previous: datetime + countdown: int, current: datetime.datetime, previous: datetime.datetime ): """Update the turn off timestamp only if necessary.""" if countdown is not None and countdown > 0: @@ -671,7 +682,7 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): @staticmethod def delayed_turn_off_timestamp( - countdown: int, current: datetime, previous: datetime + countdown: int, current: datetime.datetime, previous: datetime.datetime ): """Update the turn off timestamp only if necessary.""" if countdown is not None and countdown > 0: @@ -783,7 +794,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): return 588 @property - def hs_color(self) -> tuple: + def hs_color(self) -> tuple[float, float] | None: """Return the hs color value.""" return self._hs_color diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index df0c953d62a..50feacf5da7 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,6 +1,7 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5) and Humidifier.""" from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass import logging @@ -470,7 +471,7 @@ FAN_V2_V3_SENSORS = ( FAN_ZA5_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) -MODEL_TO_SENSORS_MAP = { +MODEL_TO_SENSORS_MAP: dict[str, tuple[str, ...]] = { MODEL_AIRFRESH_A1: AIRFRESH_SENSORS_A1, MODEL_AIRFRESH_VA2: AIRFRESH_SENSORS, MODEL_AIRFRESH_T2017: AIRFRESH_SENSORS_T2017, @@ -661,7 +662,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi sensor from a config entry.""" - entities = [] + entities: list[SensorEntity] = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] @@ -715,7 +716,7 @@ async def async_setup_entry( ) else: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - sensors = [] + sensors: Iterable[str] = [] if model in MODEL_TO_SENSORS_MAP: sensors = MODEL_TO_SENSORS_MAP[model] elif model in MODELS_HUMIDIFIER_MIOT: diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 05d6543e93d..e3d34e8c512 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -201,12 +201,20 @@ MODEL_TO_FEATURES_MAP = { @dataclass -class XiaomiMiioSwitchDescription(SwitchEntityDescription): +class XiaomiMiioSwitchRequiredKeyMixin: + """A class that describes switch entities.""" + + feature: int + method_on: str + method_off: str + + +@dataclass +class XiaomiMiioSwitchDescription( + SwitchEntityDescription, XiaomiMiioSwitchRequiredKeyMixin +): """A class that describes switch entities.""" - feature: int | None = None - method_on: str | None = None - method_off: str | None = None available_with_device_off: bool = True @@ -443,7 +451,7 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): async def async_service_handler(service: ServiceCall) -> None: """Map services to methods on XiaomiPlugGenericSwitch.""" - method = SERVICE_TO_METHOD.get(service.service) + method = SERVICE_TO_METHOD[service.service] params = { key: value for key, value in service.data.items() @@ -480,6 +488,8 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Representation of a Xiaomi Plug Generic.""" + entity_description: XiaomiMiioSwitchDescription + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id, coordinator) diff --git a/mypy.ini b/mypy.ini index 57544f298ab..53ebc8c74ce 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2863,33 +2863,3 @@ ignore_errors = true [mypy-homeassistant.components.xbox.sensor] ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.air_quality] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.device] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.fan] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.humidifier] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.light] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.sensor] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.switch] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 74a921c8351..fc3e26778a4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -89,16 +89,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xbox.browse_media", "homeassistant.components.xbox.media_source", "homeassistant.components.xbox.sensor", - "homeassistant.components.xiaomi_miio", - "homeassistant.components.xiaomi_miio.air_quality", - "homeassistant.components.xiaomi_miio.binary_sensor", - "homeassistant.components.xiaomi_miio.device", - "homeassistant.components.xiaomi_miio.device_tracker", - "homeassistant.components.xiaomi_miio.fan", - "homeassistant.components.xiaomi_miio.humidifier", - "homeassistant.components.xiaomi_miio.light", - "homeassistant.components.xiaomi_miio.sensor", - "homeassistant.components.xiaomi_miio.switch", ] # Component modules which should set no_implicit_reexport = true. From ea1fc6b5d3fbaed043ec2ad576a9e91120ca271c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 9 Jul 2022 11:59:36 -0600 Subject: [PATCH 258/820] Migrate SimpliSafe to new entity naming style (#74763) --- homeassistant/components/simplisafe/__init__.py | 13 ++++++------- .../components/simplisafe/binary_sensor.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 14a0d6dd8e9..716163c4957 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -113,8 +113,7 @@ ATTR_SYSTEM_ID = "system_id" ATTR_TIMESTAMP = "timestamp" DEFAULT_CONFIG_URL = "https://webapp.simplisafe.com/new/#/dashboard" -DEFAULT_ENTITY_MODEL = "alarm_control_panel" -DEFAULT_ENTITY_NAME = "Alarm Control Panel" +DEFAULT_ENTITY_MODEL = "Alarm control panel" DEFAULT_ERROR_THRESHOLD = 2 DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 @@ -660,6 +659,8 @@ class SimpliSafe: class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" + _attr_has_entity_name = True + def __init__( self, simplisafe: SimpliSafe, @@ -679,12 +680,11 @@ class SimpliSafeEntity(CoordinatorEntity): self._error_count = 0 if device: - model = device.type.name - device_name = device.name + model = device.type.name.capitalize().replace("_", " ") + device_name = f"{device.name.capitalize()} {model}" serial = device.serial else: - model = DEFAULT_ENTITY_MODEL - device_name = DEFAULT_ENTITY_NAME + model = device_name = DEFAULT_ENTITY_MODEL serial = system.serial event = simplisafe.initial_event_to_use[system.system_id] @@ -714,7 +714,6 @@ class SimpliSafeEntity(CoordinatorEntity): via_device=(DOMAIN, system.system_id), ) - self._attr_name = f"{system.address} {device_name} {' '.join([w.title() for w in model.split('_')])}" self._attr_unique_id = serial self._device = device self._online = True diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index 3e4c64e8658..239f5468cb3 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -103,7 +103,7 @@ class BatteryBinarySensor(SimpliSafeEntity, BinarySensorEntity): """Initialize.""" super().__init__(simplisafe, system, device=sensor) - self._attr_name = f"{super().name} Battery" + self._attr_name = "Battery" self._attr_unique_id = f"{super().unique_id}-battery" self._device: SensorV3 From ac21dc929f473107573d7163ecf953e4c4095c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 9 Jul 2022 20:00:24 +0200 Subject: [PATCH 259/820] Update aioairzone to v0.4.6 (#74810) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index e189ae741ad..e4f94327708 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.5"], + "requirements": ["aioairzone==0.4.6"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 4cdf5deb9d8..902df711bd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -113,7 +113,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.5 +aioairzone==0.4.6 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 732bf76c8df..b9370052c10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.5 +aioairzone==0.4.6 # homeassistant.components.ambient_station aioambient==2021.11.0 From 0b1f29b2b9945223a6b0ce7441eb9934dfcbf66e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 20:01:39 +0200 Subject: [PATCH 260/820] Remove nilu from mypy ignore list (#74412) --- homeassistant/components/nilu/air_quality.py | 35 ++++++++++---------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index dd60afe2ff6..400c9f0ab8e 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -121,21 +121,22 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the NILU air quality sensor.""" - name = config.get(CONF_NAME) - area = config.get(CONF_AREA) - stations = config.get(CONF_STATION) - show_on_map = config.get(CONF_SHOW_ON_MAP) + name: str = config[CONF_NAME] + area: str | None = config.get(CONF_AREA) + stations: list[str] | None = config.get(CONF_STATION) + show_on_map: bool = config[CONF_SHOW_ON_MAP] sensors = [] if area: stations = lookup_stations_in_area(area) - elif not area and not stations: + elif not stations: latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) location_client = create_location_client(latitude, longitude) stations = location_client.station_names + assert stations is not None for station in stations: client = NiluData(create_station_client(station)) client.update() @@ -195,61 +196,61 @@ class NiluSensor(AirQualityEntity): return self._name @property - def air_quality_index(self) -> str: + def air_quality_index(self) -> str | None: """Return the Air Quality Index (AQI).""" return self._max_aqi @property - def carbon_monoxide(self) -> str: + def carbon_monoxide(self) -> str | None: """Return the CO (carbon monoxide) level.""" return self.get_component_state(CO) @property - def carbon_dioxide(self) -> str: + def carbon_dioxide(self) -> str | None: """Return the CO2 (carbon dioxide) level.""" return self.get_component_state(CO2) @property - def nitrogen_oxide(self) -> str: + def nitrogen_oxide(self) -> str | None: """Return the N2O (nitrogen oxide) level.""" return self.get_component_state(NOX) @property - def nitrogen_monoxide(self) -> str: + def nitrogen_monoxide(self) -> str | None: """Return the NO (nitrogen monoxide) level.""" return self.get_component_state(NO) @property - def nitrogen_dioxide(self) -> str: + def nitrogen_dioxide(self) -> str | None: """Return the NO2 (nitrogen dioxide) level.""" return self.get_component_state(NO2) @property - def ozone(self) -> str: + def ozone(self) -> str | None: """Return the O3 (ozone) level.""" return self.get_component_state(OZONE) @property - def particulate_matter_2_5(self) -> str: + def particulate_matter_2_5(self) -> str | None: """Return the particulate matter 2.5 level.""" return self.get_component_state(PM25) @property - def particulate_matter_10(self) -> str: + def particulate_matter_10(self) -> str | None: """Return the particulate matter 10 level.""" return self.get_component_state(PM10) @property - def particulate_matter_0_1(self) -> str: + def particulate_matter_0_1(self) -> str | None: """Return the particulate matter 0.1 level.""" return self.get_component_state(PM1) @property - def sulphur_dioxide(self) -> str: + def sulphur_dioxide(self) -> str | None: """Return the SO2 (sulphur dioxide) level.""" return self.get_component_state(SO2) - def get_component_state(self, component_name: str) -> str: + def get_component_state(self, component_name: str) -> str | None: """Return formatted value of specified component.""" if component_name in self._api.data.sensors: sensor = self._api.data.sensors[component_name] diff --git a/mypy.ini b/mypy.ini index 53ebc8c74ce..a8af4b574f4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2729,9 +2729,6 @@ ignore_errors = true [mypy-homeassistant.components.minecraft_server.sensor] ignore_errors = true -[mypy-homeassistant.components.nilu.air_quality] -ignore_errors = true - [mypy-homeassistant.components.nzbget] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index fc3e26778a4..e49f16455f7 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -44,7 +44,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", - "homeassistant.components.nilu.air_quality", "homeassistant.components.nzbget", "homeassistant.components.nzbget.config_flow", "homeassistant.components.nzbget.coordinator", From 7cac1ae6a34a86ade3ba827757158aa615906d92 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 20:03:31 +0200 Subject: [PATCH 261/820] Use instance attributes in ign_sismologia (#74399) --- .../components/ign_sismologia/geo_location.py | 112 +++++++----------- 1 file changed, 43 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index cca3e61a5a3..1194c58d2ca 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -1,15 +1,19 @@ """Support for IGN Sismologia (Earthquakes) Feeds.""" from __future__ import annotations +from collections.abc import Callable from datetime import timedelta import logging +from typing import Any -from georss_ign_sismologia_client import IgnSismologiaFeedManager +from georss_ign_sismologia_client import ( + IgnSismologiaFeedEntry, + IgnSismologiaFeedManager, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, @@ -17,7 +21,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -61,19 +65,19 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the IGN Sismologia Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE] + radius_in_km: float = config[CONF_RADIUS] + minimum_magnitude: float = config[CONF_MINIMUM_MAGNITUDE] # Initialize the entity manager. feed = IgnSismologiaFeedEntityManager( hass, add_entities, scan_interval, coordinates, radius_in_km, minimum_magnitude ) - def start_feed_manager(event): + def start_feed_manager(event: Event) -> None: """Start feed manager.""" feed.startup() @@ -85,13 +89,13 @@ class IgnSismologiaFeedEntityManager: def __init__( self, - hass, - add_entities, - scan_interval, - coordinates, - radius_in_km, - minimum_magnitude, - ): + hass: HomeAssistant, + add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + radius_in_km: float, + minimum_magnitude: float, + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass @@ -106,32 +110,32 @@ class IgnSismologiaFeedEntityManager: self._add_entities = add_entities self._scan_interval = scan_interval - def startup(self): + def startup(self) -> None: """Start up this manager.""" self._feed_manager.update() self._init_regular_updates() - def _init_regular_updates(self): + def _init_regular_updates(self) -> None: """Schedule regular updates at the specified interval.""" track_time_interval( self._hass, lambda now: self._feed_manager.update(), self._scan_interval ) - def get_entry(self, external_id): + def get_entry(self, external_id: str) -> IgnSismologiaFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = IgnSismologiaLocationEvent(self, external_id) # Add new entities to HA. self._add_entities([new_entity], True) - def _update_entity(self, external_id): + def _update_entity(self, external_id: str) -> None: """Update entity.""" dispatcher_send(self._hass, f"ign_sismologia_update_{external_id}") - def _remove_entity(self, external_id): + def _remove_entity(self, external_id: str) -> None: """Remove entity.""" dispatcher_send(self._hass, f"ign_sismologia_delete_{external_id}") @@ -139,25 +143,26 @@ class IgnSismologiaFeedEntityManager: class IgnSismologiaLocationEvent(GeolocationEvent): """This represents an external event with IGN Sismologia feed data.""" + _attr_icon = "mdi:pulse" + _attr_should_poll = False + _attr_source = SOURCE _attr_unit_of_measurement = LENGTH_KILOMETERS - def __init__(self, feed_manager, external_id): + def __init__( + self, feed_manager: IgnSismologiaFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._region = None self._magnitude = None self._publication_date = None self._image_url = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -171,51 +176,36 @@ class IgnSismologiaLocationEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for IGN Sismologia feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: IgnSismologiaFeedEntry) -> None: """Update the internal state from the provided feed entry.""" self._title = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._region = feed_entry.region self._magnitude = feed_entry.magnitude self._publication_date = feed_entry.published self._image_url = feed_entry.image_url - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - @property def name(self) -> str | None: """Return the name of the entity.""" @@ -228,22 +218,7 @@ class IgnSismologiaLocationEvent(GeolocationEvent): return self._title @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( @@ -251,7 +226,6 @@ class IgnSismologiaLocationEvent(GeolocationEvent): (ATTR_TITLE, self._title), (ATTR_REGION, self._region), (ATTR_MAGNITUDE, self._magnitude), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_PUBLICATION_DATE, self._publication_date), (ATTR_IMAGE_URL, self._image_url), ): From 2cc9db5468d2ae41084b4d177152c200bc0f6ffe Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 20:30:48 +0200 Subject: [PATCH 262/820] Make deCONZ utilise forward_entry_setups (#74823) --- homeassistant/components/deconz/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 112f29db333..cb4715f3c28 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -46,12 +46,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway( hass, config_entry, api ) + await gateway.async_update_device_registry() config_entry.add_update_listener(gateway.async_config_entry_updated) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) await async_setup_events(gateway) - await gateway.async_update_device_registry() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if len(hass.data[DOMAIN]) == 1: async_setup_services(hass) From 5b32eea3d04d223b01efddb5c13a88e540df8741 Mon Sep 17 00:00:00 2001 From: simeon-simsoft <61541002+simeon-simsoft@users.noreply.github.com> Date: Sat, 9 Jul 2022 19:41:39 +0100 Subject: [PATCH 263/820] Add support for bidirectional chargers to Wallbox integration (#74313) * Add support for the Quasar bidirectional charger to the Wallbox integration, including ability to control charger while discharging, set a negative charge rate and monitor discharged amount * Make code more generic in order to support other bidirectional models in the future * Updates to files to comply with HA formatting rules * Change const file to fix black check failure * Remove unnecessay loop in number entity --- homeassistant/components/wallbox/const.py | 3 +++ homeassistant/components/wallbox/number.py | 22 ++++++++++++++++------ homeassistant/components/wallbox/sensor.py | 9 +++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 0e4e1477911..9d1db637879 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -3,7 +3,10 @@ from homeassistant.backports.enum import StrEnum DOMAIN = "wallbox" +BIDIRECTIONAL_MODEL_PREFIXES = ["QSX"] + CONF_STATION = "station" +CHARGER_ADDED_DISCHARGED_ENERGY_KEY = "added_discharged_energy" CHARGER_ADDED_ENERGY_KEY = "added_energy" CHARGER_ADDED_RANGE_KEY = "added_range" CHARGER_CHARGING_POWER_KEY = "charging_power" diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 1db791fd389..5470ec11532 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -1,4 +1,4 @@ -"""Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" +"""Home Assistant component for accessing the Wallbox Portal API. The number component allows control of charging current.""" from __future__ import annotations from dataclasses import dataclass @@ -11,9 +11,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import InvalidAuth, WallboxCoordinator, WallboxEntity from .const import ( + BIDIRECTIONAL_MODEL_PREFIXES, CHARGER_DATA_KEY, CHARGER_MAX_AVAILABLE_POWER_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_PART_NUMBER_KEY, CHARGER_SERIAL_NUMBER_KEY, DOMAIN, ) @@ -21,14 +23,13 @@ from .const import ( @dataclass class WallboxNumberEntityDescription(NumberEntityDescription): - """Describes Wallbox sensor entity.""" + """Describes Wallbox number entity.""" NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { CHARGER_MAX_CHARGING_CURRENT_KEY: WallboxNumberEntityDescription( key=CHARGER_MAX_CHARGING_CURRENT_KEY, name="Max. Charging Current", - native_min_value=6, ), } @@ -36,7 +37,7 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Create wallbox sensor entities in HASS.""" + """Create wallbox number entities in HASS.""" coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] # Check if the user is authorized to change current, if so, add number component: try: @@ -66,21 +67,30 @@ class WallboxNumber(WallboxEntity, NumberEntity): entry: ConfigEntry, description: WallboxNumberEntityDescription, ) -> None: - """Initialize a Wallbox sensor.""" + """Initialize a Wallbox number entity.""" super().__init__(coordinator) self.entity_description = description self._coordinator = coordinator self._attr_name = f"{entry.title} {description.name}" self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}" + self._is_bidirectional = ( + coordinator.data[CHARGER_DATA_KEY][CHARGER_PART_NUMBER_KEY][0:3] + in BIDIRECTIONAL_MODEL_PREFIXES + ) @property def native_max_value(self) -> float: """Return the maximum available current.""" return cast(float, self._coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY]) + @property + def native_min_value(self) -> float: + """Return the minimum available current based on charger type - some chargers can discharge.""" + return (self.max_value * -1) if self._is_bidirectional else 6 + @property def native_value(self) -> float | None: - """Return the state of the sensor.""" + """Return the value of the entity.""" return cast( Optional[float], self._coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY] ) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index e3598ca7e07..2c4a8c67bed 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -25,6 +25,7 @@ from homeassistant.helpers.typing import StateType from . import WallboxCoordinator, WallboxEntity from .const import ( + CHARGER_ADDED_DISCHARGED_ENERGY_KEY, CHARGER_ADDED_ENERGY_KEY, CHARGER_ADDED_RANGE_KEY, CHARGER_CHARGING_POWER_KEY, @@ -94,6 +95,14 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + CHARGER_ADDED_DISCHARGED_ENERGY_KEY: WallboxSensorEntityDescription( + key=CHARGER_ADDED_DISCHARGED_ENERGY_KEY, + name="Discharged Energy", + precision=2, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), CHARGER_COST_KEY: WallboxSensorEntityDescription( key=CHARGER_COST_KEY, icon="mdi:ev-station", From cdcfd1149e7ee77686eb2a2f3f8a6d0c8c33f4af Mon Sep 17 00:00:00 2001 From: Stephan Uhle Date: Sat, 9 Jul 2022 22:22:30 +0200 Subject: [PATCH 264/820] Fixed unit of measurement. #70121 (#74838) --- homeassistant/components/edl21/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index f7a79d727a0..65603b0c8c4 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( CONF_NAME, + DEGREE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -250,6 +251,7 @@ SENSOR_UNIT_MAPPING = { "W": POWER_WATT, "A": ELECTRIC_CURRENT_AMPERE, "V": ELECTRIC_POTENTIAL_VOLT, + "°": DEGREE, } @@ -449,7 +451,7 @@ class EDL21Entity(SensorEntity): @property def native_unit_of_measurement(self): """Return the unit of measurement.""" - if (unit := self._telegram.get("unit")) is None: + if (unit := self._telegram.get("unit")) is None or unit == 0: return None return SENSOR_UNIT_MAPPING[unit] From d37ad2089412006d2a47d4ab078cf15dd3fde7b8 Mon Sep 17 00:00:00 2001 From: Michel van de Wetering Date: Sat, 9 Jul 2022 22:29:50 +0200 Subject: [PATCH 265/820] Fix mediaplayer join service groupmembers definition (#74807) --- homeassistant/components/media_player/services.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index b698b87aec6..5a513e4f3a0 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -257,11 +257,14 @@ join: group_members: name: Group members description: The players which will be synced with the target player. - example: - - "media_player.multiroom_player2" - - "media_player.multiroom_player3" + required: true + example: | + - media_player.multiroom_player2 + - media_player.multiroom_player3 selector: - object: + entity: + multiple: true + domain: media_player unjoin: description: From 16900dcef15bdb9016feabd12bfec94d61ed4df6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 22:32:57 +0200 Subject: [PATCH 266/820] Make Store a generic class (#74617) --- homeassistant/auth/auth_store.py | 5 +++-- homeassistant/auth/mfa_modules/notify.py | 10 ++++------ homeassistant/auth/mfa_modules/totp.py | 12 +++++------- homeassistant/auth/providers/homeassistant.py | 13 ++++++------- homeassistant/components/almond/__init__.py | 6 +++--- .../components/ambiclimate/climate.py | 2 +- .../components/analytics/analytics.py | 8 ++++---- homeassistant/components/camera/prefs.py | 8 ++++---- homeassistant/components/energy/data.py | 10 ++++++---- homeassistant/components/hassio/__init__.py | 4 +--- .../components/homekit_controller/storage.py | 13 +++++++------ homeassistant/components/http/__init__.py | 12 +++++++----- homeassistant/components/http/auth.py | 6 +++--- .../components/mobile_app/__init__.py | 3 ++- homeassistant/components/nest/media_source.py | 15 +++++---------- homeassistant/components/network/network.py | 10 ++++++---- .../resolution_center/issue_registry.py | 7 +++++-- .../components/smartthings/smartapp.py | 7 ++++--- homeassistant/components/trace/__init__.py | 4 +++- homeassistant/components/zha/core/store.py | 4 ++-- homeassistant/config_entries.py | 4 +++- homeassistant/core.py | 6 +++--- homeassistant/helpers/area_registry.py | 9 ++++++--- homeassistant/helpers/device_registry.py | 3 +-- homeassistant/helpers/instance_id.py | 2 +- homeassistant/helpers/restore_state.py | 2 +- homeassistant/helpers/storage.py | 18 ++++++++++-------- 27 files changed, 106 insertions(+), 97 deletions(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index baf5a8bf3b3..2597781dc60 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -46,7 +46,7 @@ class AuthStore: self._users: dict[str, models.User] | None = None self._groups: dict[str, models.Group] | None = None self._perm_lookup: PermissionLookup | None = None - self._store = Store( + self._store = Store[dict[str, list[dict[str, Any]]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._lock = asyncio.Lock() @@ -483,9 +483,10 @@ class AuthStore: jwt_key=rt_dict["jwt_key"], last_used_at=last_used_at, last_used_ip=rt_dict.get("last_used_ip"), - credential=credentials.get(rt_dict.get("credential_id")), version=rt_dict.get("version"), ) + if "credential_id" in rt_dict: + token.credential = credentials.get(rt_dict["credential_id"]) users[rt_dict["user_id"]].refresh_tokens[token.id] = token self._groups = groups diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 3872257a205..464ce495050 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -7,7 +7,7 @@ from __future__ import annotations import asyncio from collections import OrderedDict import logging -from typing import Any +from typing import Any, cast import attr import voluptuous as vol @@ -100,7 +100,7 @@ class NotifyAuthModule(MultiFactorAuthModule): """Initialize the user data store.""" super().__init__(hass, config) self._user_settings: _UsersDict | None = None - self._user_store = Store( + self._user_store = Store[dict[str, dict[str, Any]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._include = config.get(CONF_INCLUDE, []) @@ -119,10 +119,8 @@ class NotifyAuthModule(MultiFactorAuthModule): if self._user_settings is not None: return - if (data := await self._user_store.async_load()) is None or not isinstance( - data, dict - ): - data = {STORAGE_USERS: {}} + if (data := await self._user_store.async_load()) is None: + data = cast(dict[str, dict[str, Any]], {STORAGE_USERS: {}}) self._user_settings = { user_id: NotifySetting(**setting) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index e503198f08b..397a7fcd386 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from io import BytesIO -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -77,7 +77,7 @@ class TotpAuthModule(MultiFactorAuthModule): """Initialize the user data store.""" super().__init__(hass, config) self._users: dict[str, str] | None = None - self._user_store = Store( + self._user_store = Store[dict[str, dict[str, str]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._init_lock = asyncio.Lock() @@ -93,16 +93,14 @@ class TotpAuthModule(MultiFactorAuthModule): if self._users is not None: return - if (data := await self._user_store.async_load()) is None or not isinstance( - data, dict - ): - data = {STORAGE_USERS: {}} + if (data := await self._user_store.async_load()) is None: + data = cast(dict[str, dict[str, str]], {STORAGE_USERS: {}}) self._users = data.get(STORAGE_USERS, {}) async def _async_save(self) -> None: """Save data.""" - await self._user_store.async_save({STORAGE_USERS: self._users}) + await self._user_store.async_save({STORAGE_USERS: self._users or {}}) def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str: """Create a ota_secret for user.""" diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index cb95907c9b2..d190a618596 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -61,10 +61,10 @@ class Data: def __init__(self, hass: HomeAssistant) -> None: """Initialize the user data store.""" self.hass = hass - self._store = Store( + self._store = Store[dict[str, list[dict[str, str]]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) - self._data: dict[str, Any] | None = None + self._data: dict[str, list[dict[str, str]]] | None = None # Legacy mode will allow usernames to start/end with whitespace # and will compare usernames case-insensitive. # Remove in 2020 or when we launch 1.0. @@ -80,10 +80,8 @@ class Data: async def async_load(self) -> None: """Load stored data.""" - if (data := await self._store.async_load()) is None or not isinstance( - data, dict - ): - data = {"users": []} + if (data := await self._store.async_load()) is None: + data = cast(dict[str, list[dict[str, str]]], {"users": []}) seen: set[str] = set() @@ -123,7 +121,8 @@ class Data: @property def users(self) -> list[dict[str, str]]: """Return users.""" - return self._data["users"] # type: ignore[index,no-any-return] + assert self._data is not None + return self._data["users"] def validate_login(self, username: str, password: str) -> None: """Validate a username and password. diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 15c280d9c1e..09ff85491ba 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -5,7 +5,7 @@ import asyncio from datetime import timedelta import logging import time -from typing import Optional, cast +from typing import Any from aiohttp import ClientError, ClientSession import async_timeout @@ -167,8 +167,8 @@ async def _configure_almond_for_ha( return _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url) - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) - data = cast(Optional[dict], await store.async_load()) + store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + data = await store.async_load() if data is None: data = {} diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 93d9348655f..50135693ff4 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -64,7 +64,7 @@ async def async_setup_entry( """Set up the Ambiclimate device from config entry.""" config = entry.data websession = async_get_clientsession(hass) - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) token_info = await store.async_load() oauth = ambiclimate.AmbiclimateOAuth( diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 802aa33585a..5bb0368b021 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -1,6 +1,6 @@ """Analytics helper class for the analytics integration.""" import asyncio -from typing import cast +from typing import Any import uuid import aiohttp @@ -66,12 +66,12 @@ class Analytics: """Initialize the Analytics class.""" self.hass: HomeAssistant = hass self.session = async_get_clientsession(hass) - self._data: dict = { + self._data: dict[str, Any] = { ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None, } - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) @property def preferences(self) -> dict: @@ -109,7 +109,7 @@ class Analytics: async def load(self) -> None: """Load preferences.""" - stored = cast(dict, await self._store.async_load()) + stored = await self._store.async_load() if stored: self._data = stored diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 3d54c10d09a..08c57631a1b 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -36,14 +36,14 @@ class CameraPreferences: def __init__(self, hass: HomeAssistant) -> None: """Initialize camera prefs.""" self._hass = hass - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = Store[dict[str, dict[str, bool]]]( + hass, STORAGE_VERSION, STORAGE_KEY + ) self._prefs: dict[str, dict[str, bool]] | None = None async def async_initialize(self) -> None: """Finish initializing the preferences.""" - if (prefs := await self._store.async_load()) is None or not isinstance( - prefs, dict - ): + if (prefs := await self._store.async_load()) is None: prefs = {} self._prefs = prefs diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index d33f915628d..e8c62da0c3c 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import Counter from collections.abc import Awaitable, Callable -from typing import Literal, Optional, TypedDict, Union, cast +from typing import Literal, TypedDict, Union import voluptuous as vol @@ -263,13 +263,15 @@ class EnergyManager: def __init__(self, hass: HomeAssistant) -> None: """Initialize energy manager.""" self._hass = hass - self._store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = storage.Store[EnergyPreferences]( + hass, STORAGE_VERSION, STORAGE_KEY + ) self.data: EnergyPreferences | None = None self._update_listeners: list[Callable[[], Awaitable]] = [] async def async_initialize(self) -> None: """Initialize the energy integration.""" - self.data = cast(Optional[EnergyPreferences], await self._store.async_load()) + self.data = await self._store.async_load() @staticmethod def default_preferences() -> EnergyPreferences: @@ -294,7 +296,7 @@ class EnergyManager: data[key] = update[key] # type: ignore[literal-required] self.data = data - self._store.async_delay_save(lambda: cast(dict, self.data), 60) + self._store.async_delay_save(lambda: data, 60) if not self._update_listeners: return diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index d580847646d..46592cbc20c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -533,12 +533,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if not await hassio.is_connected(): _LOGGER.warning("Not connected with the supervisor / system too busy!") - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, str]](hass, STORAGE_VERSION, STORAGE_KEY) if (data := await store.async_load()) is None: data = {} - assert isinstance(data, dict) - refresh_token = None if "hassio_user" in data: user = await hass.auth.async_get_user(data["hassio_user"]) diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 9372764a88a..ff39c52627e 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, TypedDict, cast +from typing import Any, TypedDict from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store @@ -46,7 +46,9 @@ class EntityMapStorage: def __init__(self, hass: HomeAssistant) -> None: """Create a new entity map store.""" self.hass = hass - self.store = Store(hass, ENTITY_MAP_STORAGE_VERSION, ENTITY_MAP_STORAGE_KEY) + self.store = Store[StorageLayout]( + hass, ENTITY_MAP_STORAGE_VERSION, ENTITY_MAP_STORAGE_KEY + ) self.storage_data: dict[str, Pairing] = {} async def async_initialize(self) -> None: @@ -55,8 +57,7 @@ class EntityMapStorage: # There is no cached data about HomeKit devices yet return - storage = cast(StorageLayout, raw_storage) - self.storage_data = storage.get("pairings", {}) + self.storage_data = raw_storage.get("pairings", {}) def get_map(self, homekit_id: str) -> Pairing | None: """Get a pairing cache item.""" @@ -87,6 +88,6 @@ class EntityMapStorage: self.store.async_delay_save(self._data_to_save, ENTITY_MAP_SAVE_DELAY) @callback - def _data_to_save(self) -> dict[str, Any]: + def _data_to_save(self) -> StorageLayout: """Return data of entity map to store in a file.""" - return {"pairings": self.storage_data} + return StorageLayout(pairings=self.storage_data) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 374e69975ce..7c8594bdd90 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -7,7 +7,7 @@ import logging import os import ssl from tempfile import NamedTemporaryFile -from typing import Any, Final, Optional, TypedDict, Union, cast +from typing import Any, Final, TypedDict, Union, cast from aiohttp import web from aiohttp.typedefs import StrOrURL @@ -125,10 +125,10 @@ class ConfData(TypedDict, total=False): @bind_hass -async def async_get_last_config(hass: HomeAssistant) -> dict | None: +async def async_get_last_config(hass: HomeAssistant) -> dict[str, Any] | None: """Return the last known working config.""" - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) - return cast(Optional[dict], await store.async_load()) + store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + return await store.async_load() class ApiConfig: @@ -475,7 +475,9 @@ async def start_http_server_and_save_config( await server.start() # If we are set up successful, we store the HTTP settings for safe mode. - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + store: storage.Store[dict[str, Any]] = storage.Store( + hass, STORAGE_VERSION, STORAGE_KEY + ) if CONF_TRUSTED_PROXIES in conf: conf[CONF_TRUSTED_PROXIES] = [ diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 18f68cc386f..7c6f445ce80 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -6,7 +6,7 @@ from datetime import timedelta from ipaddress import ip_address import logging import secrets -from typing import Final +from typing import Any, Final from aiohttp import hdrs from aiohttp.web import Application, Request, StreamResponse, middleware @@ -118,8 +118,8 @@ def async_user_not_allowed_do_auth( async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: """Create auth middleware for the app.""" - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - if (data := await store.async_load()) is None or not isinstance(data, dict): + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + if (data := await store.async_load()) is None: data = {} refresh_token = None diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 7aac961042b..70c23da66e2 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,5 +1,6 @@ """Integrates Native Apps to Home Assistant.""" from contextlib import suppress +from typing import Any from homeassistant.components import cloud, notify as hass_notify from homeassistant.components.webhook import ( @@ -38,7 +39,7 @@ PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the mobile app component.""" - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) if (app_config := await store.async_load()) is None or not isinstance( app_config, dict ): diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 4614d4b1ed4..7b5b96b7145 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -22,6 +22,7 @@ from collections.abc import Mapping from dataclasses import dataclass import logging import os +from typing import Any from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait from google_nest_sdm.device import Device @@ -89,7 +90,7 @@ async def async_get_media_event_store( os.makedirs(media_path, exist_ok=True) await hass.async_add_executor_job(mkdir) - store = Store(hass, STORAGE_VERSION, STORAGE_KEY, private=True) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY, private=True) return NestEventMediaStore(hass, subscriber, store, media_path) @@ -119,7 +120,7 @@ class NestEventMediaStore(EventMediaStore): self, hass: HomeAssistant, subscriber: GoogleNestSubscriber, - store: Store, + store: Store[dict[str, Any]], media_path: str, ) -> None: """Initialize NestEventMediaStore.""" @@ -127,7 +128,7 @@ class NestEventMediaStore(EventMediaStore): self._subscriber = subscriber self._store = store self._media_path = media_path - self._data: dict | None = None + self._data: dict[str, Any] | None = None self._devices: Mapping[str, str] | None = {} async def async_load(self) -> dict | None: @@ -137,15 +138,9 @@ class NestEventMediaStore(EventMediaStore): if (data := await self._store.async_load()) is None: _LOGGER.debug("Loaded empty event store") self._data = {} - elif isinstance(data, dict): + else: _LOGGER.debug("Loaded event store with %d records", len(data)) self._data = data - else: - raise ValueError( - "Unexpected data in storage version={}, key={}".format( - STORAGE_VERSION, STORAGE_KEY - ) - ) return self._data async def async_save(self, data: dict) -> None: diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index b2caf6438bd..e9542ec2d54 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.singleton import singleton @@ -38,8 +38,10 @@ class Network: def __init__(self, hass: HomeAssistant) -> None: """Initialize the Network class.""" - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) - self._data: dict[str, Any] = {} + self._store = Store[dict[str, list[str]]]( + hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) + self._data: dict[str, list[str]] = {} self.adapters: list[Adapter] = [] @property @@ -67,7 +69,7 @@ class Network: async def async_load(self) -> None: """Load config.""" if stored := await self._store.async_load(): - self._data = cast(dict, stored) + self._data = stored async def _async_save(self) -> None: """Save preferences.""" diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py index d97ad73bbac..7d5bbb482ba 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -2,7 +2,7 @@ from __future__ import annotations import dataclasses -from typing import cast +from typing import Optional, cast from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant, callback @@ -39,7 +39,9 @@ class IssueRegistry: """Initialize the issue registry.""" self.hass = hass self.issues: dict[tuple[str, str], IssueEntry] = {} - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) + self._store = Store[dict[str, list[dict[str, Optional[str]]]]]( + hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) @callback def async_get_issue(self, domain: str, issue_id: str) -> IssueEntry | None: @@ -119,6 +121,7 @@ class IssueRegistry: if isinstance(data, dict): for issue in data["issues"]: + assert issue["domain"] and issue["issue_id"] issues[(issue["domain"], issue["issue_id"])] = IssueEntry( active=False, breaks_in_ha_version=None, diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index fbd63d41373..adf0426e9a2 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -3,6 +3,7 @@ import asyncio import functools import logging import secrets +from typing import Any from urllib.parse import urlparse from uuid import uuid4 @@ -211,8 +212,8 @@ async def setup_smartapp_endpoint(hass: HomeAssistant): return # Get/create config to store a unique id for this hass instance. - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - if not (config := await store.async_load()) or not isinstance(config, dict): + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + if not (config := await store.async_load()): # Create config config = { CONF_INSTANCE_ID: str(uuid4()), @@ -283,7 +284,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistant): if cloudhook_url and cloud.async_is_logged_in(hass): await cloud.async_delete_cloudhook(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) # Remove cloudhook from storage - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) await store.async_save( { CONF_INSTANCE_ID: hass.data[DOMAIN][CONF_INSTANCE_ID], diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 14783fd3f84..3761ff155b4 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -52,7 +52,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the trace integration.""" hass.data[DATA_TRACE] = {} websocket_api.async_setup(hass) - store = Store(hass, STORAGE_VERSION, STORAGE_KEY, encoder=ExtendedJSONEncoder) + store = Store[dict[str, list]]( + hass, STORAGE_VERSION, STORAGE_KEY, encoder=ExtendedJSONEncoder + ) hass.data[DATA_TRACE_STORE] = store async def _async_store_traces_at_stop(*_) -> None: diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index e58dcd46dba..0b7564fe815 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -40,7 +40,7 @@ class ZhaStorage: """Initialize the zha device storage.""" self.hass: HomeAssistant = hass self.devices: MutableMapping[str, ZhaDeviceEntry] = {} - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) @callback def async_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: @@ -94,7 +94,7 @@ class ZhaStorage: async def async_load(self) -> None: """Load the registry of zha device entries.""" - data = cast(dict[str, Any], await self._store.async_load()) + data = await self._store.async_load() devices: OrderedDict[str, ZhaDeviceEntry] = OrderedDict() diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b25b62aa6e0..4b76f63681a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -845,7 +845,9 @@ class ConfigEntries: self._hass_config = hass_config self._entries: dict[str, ConfigEntry] = {} self._domain_index: dict[str, list[str]] = {} - self._store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = storage.Store[dict[str, list[dict[str, Any]]]]( + hass, STORAGE_VERSION, STORAGE_KEY + ) EntityRegistryDisabledHandler(hass).async_setup() @callback diff --git a/homeassistant/core.py b/homeassistant/core.py index b568ee72689..7b41fe476aa 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1942,7 +1942,7 @@ class Config: # pylint: disable=import-outside-toplevel from .helpers.storage import Store - store = Store( + store = Store[dict[str, Any]]( self.hass, CORE_STORAGE_VERSION, CORE_STORAGE_KEY, @@ -1950,7 +1950,7 @@ class Config: atomic_writes=True, ) - if not (data := await store.async_load()) or not isinstance(data, dict): + if not (data := await store.async_load()): return # In 2021.9 we fixed validation to disallow a path (because that's never correct) @@ -1998,7 +1998,7 @@ class Config: "currency": self.currency, } - store = Store( + store: Store[dict[str, Any]] = Store( self.hass, CORE_STORAGE_VERSION, CORE_STORAGE_KEY, diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index e5d35ccbf44..aeb52e8faed 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Container, Iterable, MutableMapping -from typing import cast +from typing import Optional, cast import attr @@ -49,7 +49,9 @@ class AreaRegistry: """Initialize the area registry.""" self.hass = hass self.areas: MutableMapping[str, AreaEntry] = {} - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) + self._store = Store[dict[str, list[dict[str, Optional[str]]]]]( + hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) self._normalized_name_area_idx: dict[str, str] = {} @callback @@ -176,8 +178,9 @@ class AreaRegistry: areas: MutableMapping[str, AreaEntry] = OrderedDict() - if isinstance(data, dict): + if data is not None: for area in data["areas"]: + assert area["name"] is not None and area["id"] is not None normalized_name = normalize_area_name(area["name"]) areas[area["id"]] = AreaEntry( name=area["name"], diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index ed3d5a7b06f..ca5e6e1aefa 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -164,7 +164,7 @@ def _async_get_device_id_from_index( return None -class DeviceRegistryStore(storage.Store): +class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): """Store entity registry data.""" async def _async_migrate_func( @@ -569,7 +569,6 @@ class DeviceRegistry: deleted_devices = OrderedDict() if data is not None: - data = cast("dict[str, Any]", data) for device in data["devices"]: devices[device["id"]] = DeviceEntry( area_id=device["area_id"], diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py index 59a4cf39498..8561d10794c 100644 --- a/homeassistant/helpers/instance_id.py +++ b/homeassistant/helpers/instance_id.py @@ -16,7 +16,7 @@ LEGACY_UUID_FILE = ".uuid" @singleton.singleton(DATA_KEY) async def async_get(hass: HomeAssistant) -> str: """Get unique ID for the hass instance.""" - store = storage.Store(hass, DATA_VERSION, DATA_KEY, True) + store = storage.Store[dict[str, str]](hass, DATA_VERSION, DATA_KEY, True) data: dict[str, str] | None = await storage.async_migrator( hass, diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index b8262d3a533..4f2d1dd0503 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -139,7 +139,7 @@ class RestoreStateData: def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" self.hass: HomeAssistant = hass - self.store: Store = Store( + self.store = Store[list[dict[str, Any]]]( hass, STORAGE_VERSION, STORAGE_KEY, encoder=JSONEncoder ) self.last_states: dict[str, StoredState] = {} diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 554a88f4ad5..6819a1eb48b 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -2,14 +2,14 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Mapping, Sequence from contextlib import suppress from copy import deepcopy import inspect from json import JSONEncoder import logging import os -from typing import Any +from typing import Any, Generic, TypeVar, Union from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback @@ -24,6 +24,8 @@ _LOGGER = logging.getLogger(__name__) STORAGE_SEMAPHORE = "storage_semaphore" +_T = TypeVar("_T", bound=Union[Mapping[str, Any], Sequence[Any]]) + @bind_hass async def async_migrator( @@ -66,7 +68,7 @@ async def async_migrator( @bind_hass -class Store: +class Store(Generic[_T]): """Class to help storing data.""" def __init__( @@ -90,7 +92,7 @@ class Store: self._unsub_delay_listener: CALLBACK_TYPE | None = None self._unsub_final_write_listener: CALLBACK_TYPE | None = None self._write_lock = asyncio.Lock() - self._load_task: asyncio.Future | None = None + self._load_task: asyncio.Future[_T | None] | None = None self._encoder = encoder self._atomic_writes = atomic_writes @@ -99,7 +101,7 @@ class Store: """Return the config path.""" return self.hass.config.path(STORAGE_DIR, self.key) - async def async_load(self) -> dict | list | None: + async def async_load(self) -> _T | None: """Load data. If the expected version and minor version do not match the given versions, the @@ -113,7 +115,7 @@ class Store: return await self._load_task - async def _async_load(self): + async def _async_load(self) -> _T | None: """Load the data and ensure the task is removed.""" if STORAGE_SEMAPHORE not in self.hass.data: self.hass.data[STORAGE_SEMAPHORE] = asyncio.Semaphore(MAX_LOAD_CONCURRENTLY) @@ -178,7 +180,7 @@ class Store: return stored - async def async_save(self, data: dict | list) -> None: + async def async_save(self, data: _T) -> None: """Save data.""" self._data = { "version": self.version, @@ -196,7 +198,7 @@ class Store: @callback def async_delay_save( self, - data_func: Callable[[], dict | list], + data_func: Callable[[], _T], delay: float = 0, ) -> None: """Save data with an optional delay.""" From 81cdbf4f9b578b5c827af6bca793d9ee6d5aa12a Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Sat, 9 Jul 2022 23:53:47 +0300 Subject: [PATCH 267/820] Bump python-gammu to 3.2.4 with Python 3.10 support (#74797) --- homeassistant/components/sms/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json index d98304ebf23..b3426c01422 100644 --- a/homeassistant/components/sms/manifest.json +++ b/homeassistant/components/sms/manifest.json @@ -3,7 +3,7 @@ "name": "SMS notifications via GSM-modem", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sms", - "requirements": ["python-gammu==3.2.3"], + "requirements": ["python-gammu==3.2.4"], "codeowners": ["@ocalvo"], "iot_class": "local_polling", "loggers": ["gammu"] diff --git a/requirements_all.txt b/requirements_all.txt index 902df711bd6..2ac21604734 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1898,7 +1898,7 @@ python-family-hub-local==0.0.2 python-forecastio==1.4.0 # homeassistant.components.sms -# python-gammu==3.2.3 +# python-gammu==3.2.4 # homeassistant.components.gc100 python-gc100==1.0.3a0 From f53bf1127f81aa919b1b157ffa4ff0dce73636b6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 9 Jul 2022 22:07:32 +0100 Subject: [PATCH 268/820] Hide homekit_controller implementation that doesn't apply to BLE (#74836) --- homeassistant/components/homekit_controller/config_flow.py | 2 +- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index b4bd66aa626..24a04874f20 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -240,7 +240,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # that the device is available and we should not wait # to retry connecting any longer. reconnect_soon # will do nothing if the device is already connected - await conn.pairing.connection.reconnect_soon() + await conn.pairing.reconnect_soon() if conn.config_num != config_num: _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 955f5e37177..6517b078454 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.20"], + "requirements": ["aiohomekit==0.7.21"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 2ac21604734..6f78c0206c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.20 +aiohomekit==0.7.21 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9370052c10..0a2ad4fcb05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.20 +aiohomekit==0.7.21 # homeassistant.components.emulated_hue # homeassistant.components.http From da133a7f05b53d4cd1fb9547f90b1addd0604c4f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:09:15 +0200 Subject: [PATCH 269/820] Remove xbox from mypy ignore list (#74504) --- homeassistant/components/xbox/__init__.py | 6 +++--- homeassistant/components/xbox/base_sensor.py | 4 ++-- homeassistant/components/xbox/binary_sensor.py | 4 ++-- homeassistant/components/xbox/browse_media.py | 8 +++++--- homeassistant/components/xbox/media_source.py | 4 ++-- homeassistant/components/xbox/sensor.py | 4 ++-- mypy.ini | 18 ------------------ script/hassfest/mypy_config.py | 6 ------ 8 files changed, 16 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index bee773d2094..6e8492e45ca 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -180,7 +180,7 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): name=DOMAIN, update_interval=timedelta(seconds=10), ) - self.data: XboxData = XboxData({}, []) + self.data: XboxData = XboxData({}, {}) self.client: XboxLiveClient = client self.consoles: SmartglassConsoleList = consoles @@ -230,7 +230,7 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): ) # Update user presence - presence_data = {} + presence_data: dict[str, PresenceData] = {} batch: PeopleResponse = await self.client.people.get_friends_own_batch( [self.client.xuid] ) @@ -262,7 +262,7 @@ def _build_presence_data(person: Person) -> PresenceData: online=person.presence_state == "Online", status=person.presence_text, in_party=person.multiplayer_summary.in_party > 0, - in_game=active_app and active_app.is_game, + in_game=active_app is not None and active_app.is_game, in_multiplayer=person.multiplayer_summary.in_multiplayer_session, gamer_score=person.gamer_score, gold_tenure=person.detail.tenure, diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 024feb294b5..5d0f3f92434 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -33,7 +33,7 @@ class XboxBaseSensorEntity(CoordinatorEntity[XboxUpdateCoordinator]): return self.coordinator.data.presence.get(self.xuid) @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the sensor.""" if not self.data: return None @@ -45,7 +45,7 @@ class XboxBaseSensorEntity(CoordinatorEntity[XboxUpdateCoordinator]): return f"{self.data.gamertag} {attr_name}" @property - def entity_picture(self) -> str: + def entity_picture(self) -> str | None: """Return the gamer pic.""" if not self.data: return None diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py index 7cf7ca6a6a5..ac97d502c55 100644 --- a/homeassistant/components/xbox/binary_sensor.py +++ b/homeassistant/components/xbox/binary_sensor.py @@ -54,7 +54,7 @@ def async_update_friends( current_ids = set(current) # Process new favorites, add them to Home Assistant - new_entities = [] + new_entities: list[XboxBinarySensorEntity] = [] for xuid in new_ids - current_ids: current[xuid] = [ XboxBinarySensorEntity(coordinator, xuid, attribute) @@ -75,7 +75,7 @@ def async_update_friends( async def async_remove_entities( xuid: str, coordinator: XboxUpdateCoordinator, - current: dict[str, XboxBinarySensorEntity], + current: dict[str, list[XboxBinarySensorEntity]], ) -> None: """Remove friend sensors from Home Assistant.""" registry = er.async_get(coordinator.hass) diff --git a/homeassistant/components/xbox/browse_media.py b/homeassistant/components/xbox/browse_media.py index b6e5a89efb3..ee1eabf1e00 100644 --- a/homeassistant/components/xbox/browse_media.py +++ b/homeassistant/components/xbox/browse_media.py @@ -1,7 +1,7 @@ """Support for media browsing.""" from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, SYSTEM_PFN_ID_MAP @@ -65,6 +65,8 @@ async def build_item_response( can_expand=True, children=[], ) + if TYPE_CHECKING: + assert library_info.children is not None # Add Home id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID @@ -84,7 +86,7 @@ async def build_item_response( title="Home", can_play=True, can_expand=False, - thumbnail=home_thumb.uri, + thumbnail=None if home_thumb is None else home_thumb.uri, ) ) @@ -107,7 +109,7 @@ async def build_item_response( title="Live TV", can_play=True, can_expand=False, - thumbnail=tv_thumb.uri, + thumbnail=None if tv_thumb is None else tv_thumb.uri, ) ) diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index c6dae46a955..21b9b25ce2d 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -55,7 +55,7 @@ def async_parse_identifier( identifier = item.identifier or "" start = ["", "", ""] items = identifier.lstrip("/").split("~~", 2) - return tuple(items + start[len(items) :]) + return tuple(items + start[len(items) :]) # type: ignore[return-value] @dataclass @@ -201,7 +201,7 @@ class XboxSource(MediaSource): ) -def _build_game_item(item: InstalledPackage, images: list[Image]): +def _build_game_item(item: InstalledPackage, images: dict[str, list[Image]]): """Build individual game.""" thumbnail = "" image = _find_media_image(images.get(item.one_store_product_id, [])) diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index 02b4f8b84a4..9cba49d1dcb 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -56,7 +56,7 @@ def async_update_friends( current_ids = set(current) # Process new favorites, add them to Home Assistant - new_entities = [] + new_entities: list[XboxSensorEntity] = [] for xuid in new_ids - current_ids: current[xuid] = [ XboxSensorEntity(coordinator, xuid, attribute) @@ -77,7 +77,7 @@ def async_update_friends( async def async_remove_entities( xuid: str, coordinator: XboxUpdateCoordinator, - current: dict[str, XboxSensorEntity], + current: dict[str, list[XboxSensorEntity]], ) -> None: """Remove friend sensors from Home Assistant.""" registry = er.async_get(coordinator.hass) diff --git a/mypy.ini b/mypy.ini index a8af4b574f4..13be5d712f4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2842,21 +2842,3 @@ ignore_errors = true [mypy-homeassistant.components.withings.config_flow] ignore_errors = true - -[mypy-homeassistant.components.xbox] -ignore_errors = true - -[mypy-homeassistant.components.xbox.base_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xbox.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xbox.browse_media] -ignore_errors = true - -[mypy-homeassistant.components.xbox.media_source] -ignore_errors = true - -[mypy-homeassistant.components.xbox.sensor] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e49f16455f7..199d6d00a7d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -82,12 +82,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.withings.binary_sensor", "homeassistant.components.withings.common", "homeassistant.components.withings.config_flow", - "homeassistant.components.xbox", - "homeassistant.components.xbox.base_sensor", - "homeassistant.components.xbox.binary_sensor", - "homeassistant.components.xbox.browse_media", - "homeassistant.components.xbox.media_source", - "homeassistant.components.xbox.sensor", ] # Component modules which should set no_implicit_reexport = true. From 3922141f5c24473c670d41dfe400cc1d8a02c53d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:11:55 +0200 Subject: [PATCH 270/820] Remove omnilogic from mypy ignore list (#74452) --- homeassistant/components/omnilogic/common.py | 3 ++- homeassistant/components/omnilogic/sensor.py | 7 +++++-- homeassistant/components/omnilogic/switch.py | 6 ++++-- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 4c92420972b..e28fd53d6fe 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from typing import Any from omnilogic import OmniLogic, OmniLogicException @@ -122,7 +123,7 @@ class OmniLogicEntity(CoordinatorEntity[OmniLogicUpdateCoordinator]): self._unique_id = unique_id self._item_id = item_id self._icon = icon - self._attrs = {} + self._attrs: dict[str, Any] = {} self._msp_system_id = msp_system_id self._backyard_name = coordinator.data[backyard_id]["BackyardName"] diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index ce9b29ef1d4..04bb1abf3e8 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -1,4 +1,6 @@ """Definition and setup of the Omnilogic Sensors for Home Assistant.""" +from typing import Any + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -34,7 +36,8 @@ async def async_setup_entry( continue for entity_setting in entity_settings: - for state_key, entity_class in entity_setting["entity_classes"].items(): + entity_classes: dict[str, type] = entity_setting["entity_classes"] + for state_key, entity_class in entity_classes.items(): if check_guard(state_key, item, entity_setting): continue @@ -248,7 +251,7 @@ class OmniLogicORPSensor(OmnilogicSensor): return orp_state -SENSOR_TYPES = { +SENSOR_TYPES: dict[tuple[int, str], list[dict[str, Any]]] = { (2, "Backyard"): [ { "entity_classes": {"airTemp": OmniLogicTemperatureSensor}, diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index 3edb4a20d33..2d2ad08d38a 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -1,5 +1,6 @@ """Platform for Omnilogic switch integration.""" import time +from typing import Any from omnilogic import OmniLogicException import voluptuous as vol @@ -34,7 +35,8 @@ async def async_setup_entry( continue for entity_setting in entity_settings: - for state_key, entity_class in entity_setting["entity_classes"].items(): + entity_classes: dict[str, type] = entity_setting["entity_classes"] + for state_key, entity_class in entity_classes.items(): if check_guard(state_key, item, entity_setting): continue @@ -229,7 +231,7 @@ class OmniLogicPumpControl(OmniLogicSwitch): raise OmniLogicException("Cannot set speed on a non-variable speed pump.") -SWITCH_TYPES = { +SWITCH_TYPES: dict[tuple[int, str], list[dict[str, Any]]] = { (4, "Relays"): [ { "entity_classes": {"switchState": OmniLogicRelayControl}, diff --git a/mypy.ini b/mypy.ini index 13be5d712f4..06dec7d4897 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2741,15 +2741,6 @@ ignore_errors = true [mypy-homeassistant.components.nzbget.switch] ignore_errors = true -[mypy-homeassistant.components.omnilogic.common] -ignore_errors = true - -[mypy-homeassistant.components.omnilogic.sensor] -ignore_errors = true - -[mypy-homeassistant.components.omnilogic.switch] -ignore_errors = true - [mypy-homeassistant.components.onvif.base] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 199d6d00a7d..4ca5b3509d4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -48,9 +48,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.nzbget.config_flow", "homeassistant.components.nzbget.coordinator", "homeassistant.components.nzbget.switch", - "homeassistant.components.omnilogic.common", - "homeassistant.components.omnilogic.sensor", - "homeassistant.components.omnilogic.switch", "homeassistant.components.onvif.base", "homeassistant.components.onvif.binary_sensor", "homeassistant.components.onvif.camera", From d49c58cf8777ad23570289676f30596214f51ff7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:14:07 +0200 Subject: [PATCH 271/820] Use instance attributes in geonetnz_quakes (#74401) --- .../geonetnz_quakes/geo_location.py | 111 ++++++------------ 1 file changed, 39 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 515fce56439..26ad780d098 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -1,12 +1,15 @@ """Geolocation support for GeoNet NZ Quakes Feeds.""" from __future__ import annotations +from collections.abc import Callable import logging +from typing import Any + +from aio_geojson_geonetnz_quakes.feed_entry import GeonetnzQuakesFeedEntry from homeassistant.components.geo_location import GeolocationEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_TIME, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, @@ -18,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import GeonetnzQuakesFeedEntityManager from .const import DOMAIN, FEED _LOGGER = logging.getLogger(__name__) @@ -40,10 +44,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the GeoNet NZ Quakes Feed platform.""" - manager = hass.data[DOMAIN][FEED][entry.entry_id] + manager: GeonetnzQuakesFeedEntityManager = hass.data[DOMAIN][FEED][entry.entry_id] @callback - def async_add_geolocation(feed_manager, integration_id, external_id): + def async_add_geolocation( + feed_manager: GeonetnzQuakesFeedEntityManager, + integration_id: str, + external_id: str, + ) -> None: """Add geolocation entity from feed.""" new_entity = GeonetnzQuakesEvent(feed_manager, integration_id, external_id) _LOGGER.debug("Adding geolocation %s", new_entity) @@ -63,27 +71,34 @@ async def async_setup_entry( class GeonetnzQuakesEvent(GeolocationEvent): """This represents an external event with GeoNet NZ Quakes feed data.""" - def __init__(self, feed_manager, integration_id, external_id): + _attr_icon = "mdi:pulse" + _attr_should_poll = False + _attr_source = SOURCE + + def __init__( + self, + feed_manager: GeonetnzQuakesFeedEntityManager, + integration_id: str, + external_id: str, + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager - self._integration_id = integration_id self._external_id = external_id - self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None + self._attr_unique_id = f"{integration_id}_{external_id}" + self._attr_unit_of_measurement = LENGTH_KILOMETERS self._depth = None self._locality = None self._magnitude = None self._mmi = None self._quality = None self._time = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._attr_unit_of_measurement = LENGTH_MILES self._remove_signal_delete = async_dispatcher_connect( self.hass, f"geonetnz_quakes_delete_{self._external_id}", @@ -105,40 +120,35 @@ class GeonetnzQuakesEvent(GeolocationEvent): entity_registry.async_remove(self.entity_id) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GeoNet NZ Quakes feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: GeonetnzQuakesFeedEntry) -> None: """Update the internal state from the provided feed entry.""" - self._title = feed_entry.title + self._attr_name = feed_entry.title # Convert distance if not metric system. if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._distance = IMPERIAL_SYSTEM.length( + self._attr_distance = IMPERIAL_SYSTEM.length( feed_entry.distance_to_home, LENGTH_KILOMETERS ) else: - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._depth = feed_entry.depth self._locality = feed_entry.locality self._magnitude = feed_entry.magnitude @@ -147,54 +157,11 @@ class GeonetnzQuakesEvent(GeolocationEvent): self._time = feed_entry.time @property - def unique_id(self) -> str | None: - """Return a unique ID containing latitude/longitude and external id.""" - return f"{self._integration_id}_{self._external_id}" - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._title - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - return LENGTH_MILES - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_DEPTH, self._depth), (ATTR_LOCALITY, self._locality), (ATTR_MAGNITUDE, self._magnitude), From d04e77ef7f1f7e0ac677f444728552d3a2adbb48 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:17:14 +0200 Subject: [PATCH 272/820] Use instance attributes in geo_json_events (#74397) --- .../geo_json_events/geo_location.py | 110 +++++++----------- 1 file changed, 42 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index 896fb7d36da..c0d6abe694f 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -1,10 +1,13 @@ """Support for generic GeoJSON events.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import logging +from typing import Any from aio_geojson_generic_client import GenericFeedManager +from aio_geojson_generic_client.feed_entry import GenericFeedEntry import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -17,7 +20,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -55,20 +58,20 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the GeoJSON Events platform.""" - url = config[CONF_URL] - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + url: str = config[CONF_URL] + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] + radius_in_km: float = config[CONF_RADIUS] # Initialize the entity manager. manager = GeoJsonFeedEntityManager( hass, async_add_entities, scan_interval, coordinates, url, radius_in_km ) await manager.async_init() - async def start_feed_manager(event=None): + async def start_feed_manager(event: Event) -> None: """Start feed manager.""" await manager.async_update() @@ -79,8 +82,14 @@ class GeoJsonFeedEntityManager: """Feed Entity Manager for GeoJSON feeds.""" def __init__( - self, hass, async_add_entities, scan_interval, coordinates, url, radius_in_km - ): + self, + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + url: str, + radius_in_km: float, + ) -> None: """Initialize the GeoJSON Feed Manager.""" self._hass = hass @@ -97,10 +106,10 @@ class GeoJsonFeedEntityManager: self._async_add_entities = async_add_entities self._scan_interval = scan_interval - async def async_init(self): + async def async_init(self) -> None: """Schedule initial and regular updates based on configured time interval.""" - async def update(event_time): + async def update(event_time: datetime) -> None: """Update.""" await self.async_update() @@ -108,26 +117,26 @@ class GeoJsonFeedEntityManager: async_track_time_interval(self._hass, update, self._scan_interval) _LOGGER.debug("Feed entity manager initialized") - async def async_update(self): + async def async_update(self) -> None: """Refresh data.""" await self._feed_manager.update() _LOGGER.debug("Feed entity manager updated") - def get_entry(self, external_id): + def get_entry(self, external_id: str) -> GenericFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - async def _generate_entity(self, external_id): + async def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = GeoJsonLocationEvent(self, external_id) # Add new entities to HA. self._async_add_entities([new_entity], True) - async def _update_entity(self, external_id): + async def _update_entity(self, external_id: str) -> None: """Update entity.""" async_dispatcher_send(self._hass, f"geo_json_events_update_{external_id}") - async def _remove_entity(self, external_id): + async def _remove_entity(self, external_id: str) -> None: """Remove entity.""" async_dispatcher_send(self._hass, f"geo_json_events_delete_{external_id}") @@ -135,18 +144,18 @@ class GeoJsonFeedEntityManager: class GeoJsonLocationEvent(GeolocationEvent): """This represents an external event with GeoJSON data.""" - def __init__(self, feed_manager, external_id): + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__(self, feed_manager: GenericFeedManager, external_id: str) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -160,68 +169,33 @@ class GeoJsonLocationEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GeoJSON location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: GenericFeedEntry) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" if not self._external_id: return {} From 36bb34f39117ce7beab2d1cfffa3b9043d9b2c29 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:18:53 +0200 Subject: [PATCH 273/820] Remove kostal_plenticore from mypy ignore list (#74433) --- .../components/kostal_plenticore/helper.py | 44 +++++++++++-------- .../components/kostal_plenticore/sensor.py | 15 ++++++- mypy.ini | 12 ----- script/hassfest/mypy_config.py | 4 -- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index c87d96161a4..ee684b68974 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Callable, Iterable +from collections.abc import Callable from datetime import datetime, timedelta import logging from typing import Any @@ -122,17 +122,20 @@ class Plenticore: class DataUpdateCoordinatorMixin: """Base implementation for read and write data.""" - async def async_read_data(self, module_id: str, data_id: str) -> list[str, bool]: + _plenticore: Plenticore + name: str + + async def async_read_data( + self, module_id: str, data_id: str + ) -> dict[str, dict[str, str]] | None: """Read data from Plenticore.""" if (client := self._plenticore.client) is None: - return False + return None try: - val = await client.get_setting_values(module_id, data_id) + return await client.get_setting_values(module_id, data_id) except PlenticoreApiException: - return False - else: - return val + return None async def async_write_data(self, module_id: str, value: dict[str, str]) -> bool: """Write settings back to Plenticore.""" @@ -170,7 +173,7 @@ class PlenticoreUpdateCoordinator(DataUpdateCoordinator): update_interval=update_inverval, ) # data ids to poll - self._fetch = defaultdict(list) + self._fetch: dict[str, list[str]] = defaultdict(list) self._plenticore = plenticore def start_fetch_data(self, module_id: str, data_id: str) -> None: @@ -246,7 +249,7 @@ class PlenticoreSelectUpdateCoordinator(DataUpdateCoordinator): update_interval=update_inverval, ) # data ids to poll - self._fetch = defaultdict(list) + self._fetch: dict[str, list[str]] = defaultdict(list) self._plenticore = plenticore def start_fetch_data(self, module_id: str, data_id: str, all_options: str) -> None: @@ -284,20 +287,23 @@ class SelectDataUpdateCoordinator( async def _async_get_current_option( self, - module_id: str | dict[str, Iterable[str]], + module_id: dict[str, list[str]], ) -> dict[str, dict[str, str]]: """Get current option.""" for mid, pids in module_id.items(): all_options = pids[1] for all_option in all_options: - if all_option != "None": - val = await self.async_read_data(mid, all_option) - for option in val.values(): - if option[all_option] == "1": - fetched = {mid: {pids[0]: all_option}} - return fetched + if all_option == "None" or not ( + val := await self.async_read_data(mid, all_option) + ): + continue + for option in val.values(): + if option[all_option] == "1": + fetched = {mid: {pids[0]: all_option}} + return fetched return {mid: {pids[0]: "None"}} + return {} class PlenticoreDataFormatter: @@ -361,7 +367,7 @@ class PlenticoreDataFormatter: return "" @staticmethod - def format_float(state: str) -> int | str: + def format_float(state: str) -> float | str: """Return the given state value as float rounded to three decimal places.""" try: return round(float(state), 3) @@ -377,7 +383,7 @@ class PlenticoreDataFormatter: return state @staticmethod - def format_inverter_state(state: str) -> str: + def format_inverter_state(state: str) -> str | None: """Return a readable string of the inverter state.""" try: value = int(state) @@ -387,7 +393,7 @@ class PlenticoreDataFormatter: return PlenticoreDataFormatter.INVERTER_STATES.get(value) @staticmethod - def format_em_manager_state(state: str) -> str: + def format_em_manager_state(state: str) -> str | None: """Return a readable state of the energy manager.""" try: value = int(state) diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 5f8fb47e85a..f66264e1d7a 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -36,7 +36,18 @@ async def async_setup_entry( timedelta(seconds=10), plenticore, ) - for module_id, data_id, name, sensor_data, fmt in SENSOR_PROCESS_DATA: + module_id: str + data_id: str + name: str + sensor_data: dict[str, Any] + fmt: str + for ( # type: ignore[assignment] + module_id, + data_id, + name, + sensor_data, + fmt, + ) in SENSOR_PROCESS_DATA: if ( module_id not in available_process_data or data_id not in available_process_data[module_id] @@ -78,7 +89,7 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): sensor_data: dict[str, Any], formatter: Callable[[str], Any], device_info: DeviceInfo, - entity_category: EntityCategory, + entity_category: EntityCategory | None, ): """Create a new Sensor Entity for Plenticore process data.""" super().__init__(coordinator) diff --git a/mypy.ini b/mypy.ini index 06dec7d4897..e6f82ba957e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2696,18 +2696,6 @@ ignore_errors = true [mypy-homeassistant.components.konnected.config_flow] ignore_errors = true -[mypy-homeassistant.components.kostal_plenticore.helper] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.select] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.sensor] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.switch] -ignore_errors = true - [mypy-homeassistant.components.lovelace] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 4ca5b3509d4..f86fd447722 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -33,10 +33,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.izone.climate", "homeassistant.components.konnected", "homeassistant.components.konnected.config_flow", - "homeassistant.components.kostal_plenticore.helper", - "homeassistant.components.kostal_plenticore.select", - "homeassistant.components.kostal_plenticore.sensor", - "homeassistant.components.kostal_plenticore.switch", "homeassistant.components.lovelace", "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", From bb974ebf7e04b7c369793fbf3744131cddb00088 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:21:58 +0200 Subject: [PATCH 274/820] Use instance attributes in gdacs (#74400) --- .../components/gdacs/geo_location.py | 103 ++++++------------ 1 file changed, 35 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 5bfbd915b6c..715ac779668 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -1,12 +1,16 @@ """Geolocation support for GDACS Feed.""" from __future__ import annotations +from collections.abc import Callable import logging +from typing import Any + +from aio_georss_gdacs import GdacsFeedManager +from aio_georss_gdacs.feed_entry import GdacsFeedEntry from homeassistant.components.geo_location import GeolocationEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, @@ -17,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import GdacsFeedEntityManager from .const import DEFAULT_ICON, DOMAIN, FEED _LOGGER = logging.getLogger(__name__) @@ -52,10 +57,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the GDACS Feed platform.""" - manager = hass.data[DOMAIN][FEED][entry.entry_id] + manager: GdacsFeedEntityManager = hass.data[DOMAIN][FEED][entry.entry_id] @callback - def async_add_geolocation(feed_manager, integration_id, external_id): + def async_add_geolocation( + feed_manager: GdacsFeedManager, integration_id: str, external_id: str + ) -> None: """Add geolocation entity from feed.""" new_entity = GdacsEvent(feed_manager, integration_id, external_id) _LOGGER.debug("Adding geolocation %s", new_entity) @@ -75,16 +82,17 @@ async def async_setup_entry( class GdacsEvent(GeolocationEvent): """This represents an external event with GDACS feed data.""" - def __init__(self, feed_manager, integration_id, external_id): + _attr_should_poll = False + _attr_source = SOURCE + + def __init__( + self, feed_manager: GdacsFeedManager, integration_id: str, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager - self._integration_id = integration_id self._external_id = external_id - self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None + self._attr_unique_id = f"{integration_id}_{external_id}" + self._attr_unit_of_measurement = LENGTH_KILOMETERS self._alert_level = None self._country = None self._description = None @@ -97,11 +105,13 @@ class GdacsEvent(GeolocationEvent): self._severity = None self._vulnerability = None self._version = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._attr_unit_of_measurement = LENGTH_MILES self._remove_signal_delete = async_dispatcher_connect( self.hass, f"gdacs_delete_{self._external_id}", self._delete_callback ) @@ -119,43 +129,38 @@ class GdacsEvent(GeolocationEvent): entity_registry.async_remove(self.entity_id) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GDACS feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: GdacsFeedEntry) -> None: """Update the internal state from the provided feed entry.""" if not (event_name := feed_entry.event_name): # Earthquakes usually don't have an event name. event_name = f"{feed_entry.country} ({feed_entry.event_id})" - self._title = f"{feed_entry.event_type}: {event_name}" + self._attr_name = f"{feed_entry.event_type}: {event_name}" # Convert distance if not metric system. if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._distance = IMPERIAL_SYSTEM.length( + self._attr_distance = IMPERIAL_SYSTEM.length( feed_entry.distance_to_home, LENGTH_KILOMETERS ) else: - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._alert_level = feed_entry.alert_level self._country = feed_entry.country self._description = feed_entry.title @@ -173,57 +178,19 @@ class GdacsEvent(GeolocationEvent): self._version = feed_entry.version @property - def unique_id(self) -> str | None: - """Return a unique ID containing latitude/longitude and external id.""" - return f"{self._integration_id}_{self._external_id}" - - @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend, if any.""" if self._event_type_short and self._event_type_short in ICONS: return ICONS[self._event_type_short] return DEFAULT_ICON @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._title - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - return LENGTH_MILES - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), (ATTR_DESCRIPTION, self._description), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_EVENT_TYPE, self._event_type), (ATTR_ALERT_LEVEL, self._alert_level), (ATTR_COUNTRY, self._country), From e529ef7b0e93c769772a971b9c52456c4e6c80f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:29:00 +0200 Subject: [PATCH 275/820] Use instance attributes in qld_bushfire (#74402) --- .../components/qld_bushfire/geo_location.py | 123 +++++++----------- 1 file changed, 47 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index 6d8f2b83bc3..e754de466d1 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -1,15 +1,19 @@ """Support for Queensland Bushfire Alert Feeds.""" from __future__ import annotations +from collections.abc import Callable from datetime import timedelta import logging +from typing import Any -from georss_qld_bushfire_alert_client import QldBushfireAlertFeedManager +from georss_qld_bushfire_alert_client import ( + QldBushfireAlertFeedEntry, + QldBushfireAlertFeedManager, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, @@ -17,7 +21,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -70,19 +74,19 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Queensland Bushfire Alert Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - categories = config[CONF_CATEGORIES] + radius_in_km: float = config[CONF_RADIUS] + categories: list[str] = config[CONF_CATEGORIES] # Initialize the entity manager. feed = QldBushfireFeedEntityManager( hass, add_entities, scan_interval, coordinates, radius_in_km, categories ) - def start_feed_manager(event): + def start_feed_manager(event: Event) -> None: """Start feed manager.""" feed.startup() @@ -93,8 +97,14 @@ class QldBushfireFeedEntityManager: """Feed Entity Manager for Qld Bushfire Alert GeoRSS feed.""" def __init__( - self, hass, add_entities, scan_interval, coordinates, radius_in_km, categories - ): + self, + hass: HomeAssistant, + add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + radius_in_km: float, + categories: list[str], + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass self._feed_manager = QldBushfireAlertFeedManager( @@ -108,32 +118,32 @@ class QldBushfireFeedEntityManager: self._add_entities = add_entities self._scan_interval = scan_interval - def startup(self): + def startup(self) -> None: """Start up this manager.""" self._feed_manager.update() self._init_regular_updates() - def _init_regular_updates(self): + def _init_regular_updates(self) -> None: """Schedule regular updates at the specified interval.""" track_time_interval( self._hass, lambda now: self._feed_manager.update(), self._scan_interval ) - def get_entry(self, external_id): + def get_entry(self, external_id: str) -> QldBushfireAlertFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = QldBushfireLocationEvent(self, external_id) # Add new entities to HA. self._add_entities([new_entity], True) - def _update_entity(self, external_id): + def _update_entity(self, external_id: str) -> None: """Update entity.""" dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - def _remove_entity(self, external_id): + def _remove_entity(self, external_id: str) -> None: """Remove entity.""" dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) @@ -141,23 +151,25 @@ class QldBushfireFeedEntityManager: class QldBushfireLocationEvent(GeolocationEvent): """This represents an external event with Qld Bushfire feed data.""" - def __init__(self, feed_manager, external_id): + _attr_icon = "mdi:fire" + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__( + self, feed_manager: QldBushfireFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._category = None self._publication_date = None self._updated_date = None self._status = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -171,84 +183,43 @@ class QldBushfireLocationEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for Qld Bushfire Alert feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: QldBushfireAlertFeedEntry) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._category = feed_entry.category self._publication_date = feed_entry.published self._updated_date = feed_entry.updated self._status = feed_entry.status @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:fire" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), (ATTR_CATEGORY, self._category), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_PUBLICATION_DATE, self._publication_date), (ATTR_UPDATED_DATE, self._updated_date), (ATTR_STATUS, self._status), From d33779d3a05acbc97b7d94a347d3323303dbf828 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Sun, 10 Jul 2022 00:33:10 +0300 Subject: [PATCH 276/820] Cleanup mikrotik device extra_attributes (#74491) --- homeassistant/components/mikrotik/const.py | 1 - .../components/mikrotik/device_tracker.py | 21 +++---------------- homeassistant/components/mikrotik/hub.py | 5 ++--- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index 2942e6981fa..bbe129c4a00 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -44,7 +44,6 @@ PLATFORMS: Final = [Platform.DEVICE_TRACKER] ATTR_DEVICE_TRACKER: Final = [ "comment", - "mac-address", "ssid", "interface", "signal-strength", diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 4e002cdbcfe..158d95dd683 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -18,10 +18,6 @@ import homeassistant.util.dt as dt_util from .const import DOMAIN from .hub import Device, MikrotikDataUpdateCoordinator -# These are normalized to ATTR_IP and ATTR_MAC to conform -# to device_tracker -FILTER_ATTRS = ("ip_address", "mac_address") - async def async_setup_entry( hass: HomeAssistant, @@ -90,6 +86,8 @@ class MikrotikDataUpdateCoordinatorTracker( """Initialize the tracked device.""" super().__init__(coordinator) self.device = device + self._attr_name = str(device.name) + self._attr_unique_id = device.mac @property def is_connected(self) -> bool: @@ -107,12 +105,6 @@ class MikrotikDataUpdateCoordinatorTracker( """Return the source type of the client.""" return SOURCE_TYPE_ROUTER - @property - def name(self) -> str: - """Return the name of the client.""" - # Stringify to ensure we return a string - return str(self.device.name) - @property def hostname(self) -> str: """Return the hostname of the client.""" @@ -128,14 +120,7 @@ class MikrotikDataUpdateCoordinatorTracker( """Return the mac address of the client.""" return self.device.ip_address - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return self.device.mac - @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" - if self.is_connected: - return {k: v for k, v in self.device.attrs.items() if k not in FILTER_ATTRS} - return None + return self.device.attrs if self.is_connected else None diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 9219159ca74..66fe7226d9b 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -77,11 +77,10 @@ class Device: @property def attrs(self) -> dict[str, Any]: """Return device attributes.""" - attr_data = self._wireless_params if self._wireless_params else self._params + attr_data = self._wireless_params | self._params for attr in ATTR_DEVICE_TRACKER: if attr in attr_data: self._attrs[slugify(attr)] = attr_data[attr] - self._attrs["ip_address"] = self._params.get("active-address") return self._attrs def update( @@ -250,7 +249,7 @@ class MikrotikData: ) -> list[dict[str, Any]]: """Retrieve data from Mikrotik API.""" try: - _LOGGER.info("Running command %s", cmd) + _LOGGER.debug("Running command %s", cmd) if params: return list(self.api(cmd=cmd, **params)) return list(self.api(cmd=cmd)) From e3242d8d16b4f1d20d4b707d8979b538b353b0e2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 23:35:06 +0200 Subject: [PATCH 277/820] Move add/remove logic of deCONZ clip sensors to gateway class (#74481) --- .../components/deconz/binary_sensor.py | 19 ----- homeassistant/components/deconz/climate.py | 18 ----- .../components/deconz/deconz_event.py | 3 - homeassistant/components/deconz/gateway.py | 77 ++++++++++++++----- homeassistant/components/deconz/lock.py | 1 + homeassistant/components/deconz/number.py | 4 +- homeassistant/components/deconz/sensor.py | 19 ----- 7 files changed, 60 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 0d090751edd..814dec443e0 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -24,7 +24,6 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.entity_registry as er @@ -215,9 +214,6 @@ async def async_setup_entry( """Add sensor from deCONZ.""" sensor = gateway.api.sensors[sensor_id] - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - return - for description in ( ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS ): @@ -236,21 +232,6 @@ async def async_setup_entry( gateway.api.sensors, ) - @callback - def async_reload_clip_sensors() -> None: - """Load clip sensor sensors from deCONZ.""" - for sensor_id, sensor in gateway.api.sensors.items(): - if sensor.type.startswith("CLIP"): - async_add_sensor(EventType.ADDED, sensor_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_clip_sensors, - async_reload_clip_sensors, - ) - ) - class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ binary sensor.""" diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 880e11f080b..75b37db2c13 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -28,7 +28,6 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE @@ -85,8 +84,6 @@ async def async_setup_entry( def async_add_climate(_: EventType, climate_id: str) -> None: """Add climate from deCONZ.""" climate = gateway.api.sensors.thermostat[climate_id] - if not gateway.option_allow_clip_sensor and climate.type.startswith("CLIP"): - return async_add_entities([DeconzThermostat(climate, gateway)]) gateway.register_platform_add_device_callback( @@ -94,21 +91,6 @@ async def async_setup_entry( gateway.api.sensors.thermostat, ) - @callback - def async_reload_clip_sensors() -> None: - """Load clip sensors from deCONZ.""" - for climate_id, climate in gateway.api.sensors.thermostat.items(): - if climate.type.startswith("CLIP"): - async_add_climate(EventType.ADDED, climate_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_clip_sensors, - async_reload_clip_sensors, - ) - ) - class DeconzThermostat(DeconzDevice, ClimateEntity): """Representation of a deCONZ thermostat.""" diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 270e66bf91d..6f7b6f9038a 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -46,9 +46,6 @@ async def async_setup_events(gateway: DeconzGateway) -> None: new_event: DeconzAlarmEvent | DeconzEvent sensor = gateway.api.sensors[sensor_id] - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - return None - if isinstance(sensor, Switch): new_event = DeconzEvent(sensor, gateway) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 25471d1448a..0ee967f88d9 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors +from pydeconz.interfaces import sensors from pydeconz.interfaces.api import APIItems, GroupedAPIItems from pydeconz.interfaces.groups import Groups from pydeconz.models.event import EventType @@ -42,6 +43,35 @@ from .errors import AuthenticationRequired, CannotConnect if TYPE_CHECKING: from .deconz_event import DeconzAlarmEvent, DeconzEvent +SENSORS = ( + sensors.SensorResourceManager, + sensors.AirPurifierHandler, + sensors.AirQualityHandler, + sensors.AlarmHandler, + sensors.AncillaryControlHandler, + sensors.BatteryHandler, + sensors.CarbonMonoxideHandler, + sensors.ConsumptionHandler, + sensors.DaylightHandler, + sensors.DoorLockHandler, + sensors.FireHandler, + sensors.GenericFlagHandler, + sensors.GenericStatusHandler, + sensors.HumidityHandler, + sensors.LightLevelHandler, + sensors.OpenCloseHandler, + sensors.PowerHandler, + sensors.PresenceHandler, + sensors.PressureHandler, + sensors.RelativeRotaryHandler, + sensors.SwitchHandler, + sensors.TemperatureHandler, + sensors.ThermostatHandler, + sensors.TimeHandler, + sensors.VibrationHandler, + sensors.WaterHandler, +) + class DeconzGateway: """Manages a single deCONZ gateway.""" @@ -60,14 +90,17 @@ class DeconzGateway: self.ignore_state_updates = False self.signal_reachable = f"deconz-reachable-{config_entry.entry_id}" - self.signal_reload_clip_sensors = f"deconz_reload_clip_{config_entry.entry_id}" self.deconz_ids: dict[str, str] = {} self.entities: dict[str, set[str]] = {} self.events: list[DeconzAlarmEvent | DeconzEvent] = [] - self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() + self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set() self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set() + self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() + self.option_allow_clip_sensor = self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ) self.option_allow_deconz_groups = config_entry.options.get( CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS ) @@ -90,20 +123,12 @@ class DeconzGateway: """Gateway which is used with deCONZ services without defining id.""" return cast(bool, self.config_entry.options[CONF_MASTER_GATEWAY]) - # Options - - @property - def option_allow_clip_sensor(self) -> bool: - """Allow loading clip sensor from gateway.""" - return self.config_entry.options.get( - CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR - ) - @callback def register_platform_add_device_callback( self, add_device_callback: Callable[[EventType, str], None], deconz_device_interface: APIItems | GroupedAPIItems, + always_ignore_clip_sensors: bool = False, ) -> None: """Wrap add_device_callback to check allow_new_devices option.""" @@ -128,6 +153,13 @@ class DeconzGateway: if not self.option_allow_deconz_groups: return + if isinstance(deconz_device_interface, SENSORS): + device = deconz_device_interface[device_id] + if device.type.startswith("CLIP") and not always_ignore_clip_sensors: + self.clip_sensors.add((async_add_device, device_id)) + if not self.option_allow_clip_sensor: + return + add_device_callback(EventType.ADDED, device_id) self.config_entry.async_on_unload( @@ -212,15 +244,20 @@ class DeconzGateway: # Allow CLIP sensors - if self.option_allow_clip_sensor: - async_dispatcher_send(self.hass, self.signal_reload_clip_sensors) - - else: - deconz_ids += [ - sensor.deconz_id - for sensor in self.api.sensors.values() - if sensor.type.startswith("CLIP") - ] + option_allow_clip_sensor = self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ) + if option_allow_clip_sensor != self.option_allow_clip_sensor: + self.option_allow_clip_sensor = option_allow_clip_sensor + if option_allow_clip_sensor: + for add_device, device_id in self.clip_sensors: + add_device(EventType.ADDED, device_id) + else: + deconz_ids += [ + sensor.deconz_id + for sensor in self.api.sensors.values() + if sensor.type.startswith("CLIP") + ] # Allow Groups diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index cf4bd7f14f5..d6f9d670c01 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -46,6 +46,7 @@ async def async_setup_entry( gateway.register_platform_add_device_callback( async_add_lock_from_sensor, gateway.api.sensors.door_lock, + always_ignore_clip_sensors=True, ) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 81f3e434007..acd890d9c90 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -65,8 +65,7 @@ async def async_setup_entry( def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add sensor from deCONZ.""" sensor = gateway.api.sensors.presence[sensor_id] - if sensor.type.startswith("CLIP"): - return + for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): if ( not hasattr(sensor, description.key) @@ -78,6 +77,7 @@ async def async_setup_entry( gateway.register_platform_add_device_callback( async_add_sensor, gateway.api.sensors.presence, + always_ignore_clip_sensors=True, ) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 2aff2b12448..15af1b3dd8f 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -39,7 +39,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -248,9 +247,6 @@ async def async_setup_entry( sensor = gateway.api.sensors[sensor_id] entities: list[DeconzSensor] = [] - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - return - if sensor.battery is None and not sensor.type.startswith("CLIP"): DeconzBatteryTracker(sensor_id, gateway, async_add_entities) @@ -276,21 +272,6 @@ async def async_setup_entry( gateway.api.sensors, ) - @callback - def async_reload_clip_sensors() -> None: - """Load clip sensor sensors from deCONZ.""" - for sensor_id, sensor in gateway.api.sensors.items(): - if sensor.type.startswith("CLIP"): - async_add_sensor(EventType.ADDED, sensor_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_clip_sensors, - async_reload_clip_sensors, - ) - ) - class DeconzSensor(DeconzDevice, SensorEntity): """Representation of a deCONZ sensor.""" From db111645c2270d7a595ad14c6e280538ac72afa3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:38:53 +0200 Subject: [PATCH 278/820] Use instance attributes in nsw_rural_fire_service_feed (#74398) --- .../geo_location.py | 135 +++++++----------- 1 file changed, 54 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index fe5d27feee2..553f73a128e 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -1,15 +1,19 @@ """Support for NSW Rural Fire Service Feeds.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import logging +from typing import Any from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeedManager +from aio_geojson_nsw_rfs_incidents.feed_entry import ( + NswRuralFireServiceIncidentsFeedEntry, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_LATITUDE, CONF_LONGITUDE, @@ -19,7 +23,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -73,23 +77,23 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the NSW Rural Fire Service Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - categories = config.get(CONF_CATEGORIES) + radius_in_km: float = config[CONF_RADIUS] + categories: list[str] = config[CONF_CATEGORIES] # Initialize the entity manager. manager = NswRuralFireServiceFeedEntityManager( hass, async_add_entities, scan_interval, coordinates, radius_in_km, categories ) - async def start_feed_manager(event): + async def start_feed_manager(event: Event) -> None: """Start feed manager.""" await manager.async_init() - async def stop_feed_manager(event): + async def stop_feed_manager(event: Event) -> None: """Stop feed manager.""" await manager.async_stop() @@ -103,13 +107,13 @@ class NswRuralFireServiceFeedEntityManager: def __init__( self, - hass, - async_add_entities, - scan_interval, - coordinates, - radius_in_km, - categories, - ): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + radius_in_km: float, + categories: list[str], + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass websession = aiohttp_client.async_get_clientsession(hass) @@ -124,12 +128,12 @@ class NswRuralFireServiceFeedEntityManager: ) self._async_add_entities = async_add_entities self._scan_interval = scan_interval - self._track_time_remove_callback = None + self._track_time_remove_callback: Callable[[], None] | None = None - async def async_init(self): + async def async_init(self) -> None: """Schedule initial and regular updates based on configured time interval.""" - async def update(event_time): + async def update(event_time: datetime) -> None: """Update.""" await self.async_update() @@ -140,32 +144,34 @@ class NswRuralFireServiceFeedEntityManager: _LOGGER.debug("Feed entity manager initialized") - async def async_update(self): + async def async_update(self) -> None: """Refresh data.""" await self._feed_manager.update() _LOGGER.debug("Feed entity manager updated") - async def async_stop(self): + async def async_stop(self) -> None: """Stop this feed entity manager from refreshing.""" if self._track_time_remove_callback: self._track_time_remove_callback() _LOGGER.debug("Feed entity manager stopped") - def get_entry(self, external_id): + def get_entry( + self, external_id: str + ) -> NswRuralFireServiceIncidentsFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - async def _generate_entity(self, external_id): + async def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = NswRuralFireServiceLocationEvent(self, external_id) # Add new entities to HA. self._async_add_entities([new_entity], True) - async def _update_entity(self, external_id): + async def _update_entity(self, external_id: str) -> None: """Update entity.""" async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - async def _remove_entity(self, external_id): + async def _remove_entity(self, external_id: str) -> None: """Remove entity.""" async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) @@ -173,15 +179,16 @@ class NswRuralFireServiceFeedEntityManager: class NswRuralFireServiceLocationEvent(GeolocationEvent): """This represents an external event with NSW Rural Fire Service data.""" - def __init__(self, feed_manager, external_id): + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__( + self, feed_manager: NswRuralFireServiceFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._category = None self._publication_date = None self._location = None @@ -191,10 +198,10 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._fire = None self._size = None self._responsible_agency = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -213,34 +220,31 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._remove_signal_update() @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for NSW Rural Fire Service location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed( + self, feed_entry: NswRuralFireServiceIncidentsFeedEntry + ) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._category = feed_entry.category self._publication_date = feed_entry.publication_date self._location = feed_entry.location @@ -252,51 +256,20 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._responsible_agency = feed_entry.responsible_agency @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend.""" if self._fire: return "mdi:fire" return "mdi:alarm-light" @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), (ATTR_CATEGORY, self._category), (ATTR_LOCATION, self._location), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_PUBLICATION_DATE, self._publication_date), (ATTR_COUNCIL_AREA, self._council_area), (ATTR_STATUS, self._status), From 63582c3f98303481fd0cb14b54ba20126d0adf0c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 23:40:15 +0200 Subject: [PATCH 279/820] Bump deCONZ dependency to fix #74791 (#74804) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 06f8b6c0376..2ae400bbe19 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==97"], + "requirements": ["pydeconz==98"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 6f78c0206c8..1842c31497f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==97 +pydeconz==98 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a2ad4fcb05..269ce43c0a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==97 +pydeconz==98 # homeassistant.components.dexcom pydexcom==0.2.3 From 81e7eb623bfae512b4d71acb58b551533daf3320 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 23:40:55 +0200 Subject: [PATCH 280/820] Migrate Twente Milieu to new entity naming style (#74593) --- .../components/twentemilieu/calendar.py | 2 +- .../components/twentemilieu/const.py | 10 ++--- .../components/twentemilieu/entity.py | 2 + .../components/twentemilieu/test_calendar.py | 4 +- tests/components/twentemilieu/test_sensor.py | 45 ++++++++++++------- 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py index 1c7cfcbd4fc..8bcf1b1d390 100644 --- a/homeassistant/components/twentemilieu/calendar.py +++ b/homeassistant/components/twentemilieu/calendar.py @@ -28,7 +28,7 @@ async def async_setup_entry( class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity): """Defines a Twente Milieu calendar.""" - _attr_name = "Twente Milieu" + _attr_has_entity_name = True _attr_icon = "mdi:delete-empty" def __init__( diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index c9f2f935772..ac7354a42f2 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -15,9 +15,9 @@ CONF_HOUSE_NUMBER = "house_number" CONF_HOUSE_LETTER = "house_letter" WASTE_TYPE_TO_DESCRIPTION = { - WasteType.NON_RECYCLABLE: "Non-recyclable Waste Pickup", - WasteType.ORGANIC: "Organic Waste Pickup", - WasteType.PACKAGES: "Packages Waste Pickup", - WasteType.PAPER: "Paper Waste Pickup", - WasteType.TREE: "Christmas Tree Pickup", + WasteType.NON_RECYCLABLE: "Non-recyclable waste pickup", + WasteType.ORGANIC: "Organic waste pickup", + WasteType.PACKAGES: "Packages waste pickup", + WasteType.PAPER: "Paper waste pickup", + WasteType.TREE: "Christmas tree pickup", } diff --git a/homeassistant/components/twentemilieu/entity.py b/homeassistant/components/twentemilieu/entity.py index 008c0fa441e..5a1a1758d3e 100644 --- a/homeassistant/components/twentemilieu/entity.py +++ b/homeassistant/components/twentemilieu/entity.py @@ -22,6 +22,8 @@ class TwenteMilieuEntity( ): """Defines a Twente Milieu entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]], diff --git a/tests/components/twentemilieu/test_calendar.py b/tests/components/twentemilieu/test_calendar.py index b9f1dd9247d..11f8a1abd75 100644 --- a/tests/components/twentemilieu/test_calendar.py +++ b/tests/components/twentemilieu/test_calendar.py @@ -27,7 +27,7 @@ async def test_waste_pickup_calendar( assert entry.unique_id == "12345" assert state.attributes[ATTR_ICON] == "mdi:delete-empty" assert state.attributes["all_day"] is True - assert state.attributes["message"] == "Christmas Tree Pickup" + assert state.attributes["message"] == "Christmas tree pickup" assert not state.attributes["location"] assert not state.attributes["description"] assert state.state == STATE_OFF @@ -78,7 +78,7 @@ async def test_api_events( assert events[0] == { "start": {"date": "2022-01-06"}, "end": {"date": "2022-01-06"}, - "summary": "Christmas Tree Pickup", + "summary": "Christmas tree pickup", "description": None, "location": None, } diff --git a/tests/components/twentemilieu/test_sensor.py b/tests/components/twentemilieu/test_sensor.py index 5f09018358e..6e20fd4d141 100644 --- a/tests/components/twentemilieu/test_sensor.py +++ b/tests/components/twentemilieu/test_sensor.py @@ -22,57 +22,72 @@ async def test_waste_pickup_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.christmas_tree_pickup") - entry = entity_registry.async_get("sensor.christmas_tree_pickup") + state = hass.states.get("sensor.twente_milieu_christmas_tree_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_christmas_tree_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_tree" assert state.state == "2022-01-06" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Christmas Tree Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Twente Milieu Christmas tree pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:pine-tree" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.non_recyclable_waste_pickup") - entry = entity_registry.async_get("sensor.non_recyclable_waste_pickup") + state = hass.states.get("sensor.twente_milieu_non_recyclable_waste_pickup") + entry = entity_registry.async_get( + "sensor.twente_milieu_non_recyclable_waste_pickup" + ) assert entry assert state assert entry.unique_id == "twentemilieu_12345_Non-recyclable" assert state.state == "2021-11-01" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Non-recyclable Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Twente Milieu Non-recyclable waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.organic_waste_pickup") - entry = entity_registry.async_get("sensor.organic_waste_pickup") + state = hass.states.get("sensor.twente_milieu_organic_waste_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_organic_waste_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_Organic" assert state.state == "2021-11-02" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Organic Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Twente Milieu Organic waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.packages_waste_pickup") - entry = entity_registry.async_get("sensor.packages_waste_pickup") + state = hass.states.get("sensor.twente_milieu_packages_waste_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_packages_waste_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_Plastic" assert state.state == "2021-11-03" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Packages Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Twente Milieu Packages waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.paper_waste_pickup") - entry = entity_registry.async_get("sensor.paper_waste_pickup") + state = hass.states.get("sensor.twente_milieu_paper_waste_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_paper_waste_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_Paper" assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Paper Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Twente Milieu Paper waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes From 0f813b61c376046a9169eb47e5fc3676246992c6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 23:41:04 +0200 Subject: [PATCH 281/820] Migrate Elgato to new entity naming style (#74590) --- homeassistant/components/elgato/entity.py | 2 ++ homeassistant/components/elgato/light.py | 1 - tests/components/elgato/test_button.py | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/elgato/entity.py b/homeassistant/components/elgato/entity.py index 8927f8d71c0..16d3ac78e81 100644 --- a/homeassistant/components/elgato/entity.py +++ b/homeassistant/components/elgato/entity.py @@ -12,6 +12,8 @@ from .const import DOMAIN class ElgatoEntity(Entity): """Defines an Elgato entity.""" + _attr_has_entity_name = True + def __init__(self, client: Elgato, info: Info, mac: str | None) -> None: """Initialize an Elgato entity.""" self.client = client diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index e13119c2887..2a9f63a83d7 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -80,7 +80,6 @@ class ElgatoLight( self._attr_min_mireds = 143 self._attr_max_mireds = 344 - self._attr_name = info.display_name or info.product_name self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} self._attr_unique_id = info.serial_number diff --git a/tests/components/elgato/test_button.py b/tests/components/elgato/test_button.py index 2ab3d7ee7c4..1f673bc6f44 100644 --- a/tests/components/elgato/test_button.py +++ b/tests/components/elgato/test_button.py @@ -25,12 +25,12 @@ async def test_button_identify( device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) - state = hass.states.get("button.identify") + state = hass.states.get("button.frenck_identify") assert state assert state.attributes.get(ATTR_ICON) == "mdi:help" assert state.state == STATE_UNKNOWN - entry = entity_registry.async_get("button.identify") + entry = entity_registry.async_get("button.frenck_identify") assert entry assert entry.unique_id == "CN11A1A00001_identify" assert entry.entity_category == EntityCategory.CONFIG @@ -53,14 +53,14 @@ async def test_button_identify( await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.identify"}, + {ATTR_ENTITY_ID: "button.frenck_identify"}, blocking=True, ) assert len(mock_elgato.identify.mock_calls) == 1 mock_elgato.identify.assert_called_with() - state = hass.states.get("button.identify") + state = hass.states.get("button.frenck_identify") assert state assert state.state == "2021-11-13T11:48:00+00:00" @@ -79,7 +79,7 @@ async def test_button_identify_error( await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.identify"}, + {ATTR_ENTITY_ID: "button.frenck_identify"}, blocking=True, ) await hass.async_block_till_done() From 157d6dc83f5c45d0ca050e53c373e0f714f52226 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Jul 2022 16:45:49 -0500 Subject: [PATCH 282/820] Add missing Start Dimmer mapping for bond buttons (#74555) --- homeassistant/components/bond/button.py | 7 +++++++ tests/components/bond/test_button.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 9a82309e347..6f57a23b079 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -88,6 +88,13 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = ( mutually_exclusive=Action.TURN_DOWN_LIGHT_ON, argument=None, ), + BondButtonEntityDescription( + key=Action.START_DIMMER, + name="Start Dimmer", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_BRIGHTNESS, + argument=None, + ), BondButtonEntityDescription( key=Action.START_UP_LIGHT_DIMMER, name="Start Up Light Dimmer", diff --git a/tests/components/bond/test_button.py b/tests/components/bond/test_button.py index 4411b25657b..24ea4730d6c 100644 --- a/tests/components/bond/test_button.py +++ b/tests/components/bond/test_button.py @@ -30,6 +30,7 @@ def light_brightness_increase_decrease_only(name: str): "actions": [ Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF, + Action.START_DIMMER, Action.START_INCREASING_BRIGHTNESS, Action.START_DECREASING_BRIGHTNESS, Action.STOP, @@ -75,6 +76,8 @@ async def test_entity_registry(hass: core.HomeAssistant): assert entity.unique_id == "test-hub-id_test-device-id_startincreasingbrightness" entity = registry.entities["button.name_1_start_decreasing_brightness"] assert entity.unique_id == "test-hub-id_test-device-id_startdecreasingbrightness" + entity = registry.entities["button.name_1_start_dimmer"] + assert entity.unique_id == "test-hub-id_test-device-id_startdimmer" async def test_mutually_exclusive_actions(hass: core.HomeAssistant): From d80d16aaa45e3d6dfe49c558e18247a67b2d45f8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 23:46:24 +0200 Subject: [PATCH 283/820] Use pydeconz interface controls for number platform (#74666) --- homeassistant/components/deconz/number.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index acd890d9c90..4c0959f950d 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -113,8 +113,10 @@ class DeconzNumber(DeconzDevice, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set sensor config.""" - data = {self.entity_description.key: int(value)} - await self._device.set_config(**data) + await self.gateway.api.sensors.presence.set_config( + id=self._device.resource_id, + delay=int(value), + ) @property def unique_id(self) -> str: From 40ee7bab8f8e027744097c32f876856768289cc1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 10 Jul 2022 00:31:09 +0200 Subject: [PATCH 284/820] Fix false-positive in pylint plugin (#74244) --- pylint/plugins/hass_enforce_type_hints.py | 67 +++++---- tests/pylint/test_enforce_type_hints.py | 159 ++++++++++++++++------ 2 files changed, 159 insertions(+), 67 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 7996afdc545..680d25414aa 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -53,13 +53,18 @@ class ClassTypeHintMatch: _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), - # x_of_y matches items such as "Awaitable[None]" - "x_of_y": re.compile(r"^(\w+)\[(.*?]*)\]$"), - # x_of_y_comma_z matches items such as "Callable[..., Awaitable[None]]" - "x_of_y_comma_z": re.compile(r"^(\w+)\[(.*?]*), (.*?]*)\]$"), - # x_of_y_of_z_comma_a matches items such as "list[dict[str, Any]]" - "x_of_y_of_z_comma_a": re.compile(r"^(\w+)\[(\w+)\[(.*?]*), (.*?]*)\]\]$"), } +_INNER_MATCH = r"((?:\w+)|(?:\.{3})|(?:\w+\[.+\]))" +_INNER_MATCH_POSSIBILITIES = [i + 1 for i in range(5)] +_TYPE_HINT_MATCHERS.update( + { + f"x_of_y_{i}": re.compile( + rf"^(\w+)\[{_INNER_MATCH}" + f", {_INNER_MATCH}" * (i - 1) + r"\]$" + ) + for i in _INNER_MATCH_POSSIBILITIES + } +) + _MODULE_REGEX: re.Pattern[str] = re.compile(r"^homeassistant\.components\.\w+(\.\w+)?$") @@ -1438,25 +1443,26 @@ def _is_valid_type( and _is_valid_type(match.group(2), node.right) ) - # Special case for xxx[yyy[zzz, aaa]]` - if match := _TYPE_HINT_MATCHERS["x_of_y_of_z_comma_a"].match(expected_type): - return ( - isinstance(node, nodes.Subscript) - and _is_valid_type(match.group(1), node.value) - and isinstance(subnode := node.slice, nodes.Subscript) - and _is_valid_type(match.group(2), subnode.value) - and isinstance(subnode.slice, nodes.Tuple) - and _is_valid_type(match.group(3), subnode.slice.elts[0]) - and _is_valid_type(match.group(4), subnode.slice.elts[1]) + # Special case for `xxx[aaa, bbb, ccc, ...] + if ( + isinstance(node, nodes.Subscript) + and isinstance(node.slice, nodes.Tuple) + and ( + match := _TYPE_HINT_MATCHERS[f"x_of_y_{len(node.slice.elts)}"].match( + expected_type + ) ) - - # Special case for xxx[yyy, zzz]` - if match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(expected_type): - # Handle special case of Mapping[xxx, Any] - if in_return and match.group(1) == "Mapping" and match.group(3) == "Any": + ): + # This special case is separate because we want Mapping[str, Any] + # to also match dict[str, int] and similar + if ( + len(node.slice.elts) == 2 + and in_return + and match.group(1) == "Mapping" + and match.group(3) == "Any" + ): return ( - isinstance(node, nodes.Subscript) - and isinstance(node.value, nodes.Name) + isinstance(node.value, nodes.Name) # We accept dict when Mapping is needed and node.value.name in ("Mapping", "dict") and isinstance(node.slice, nodes.Tuple) @@ -1464,16 +1470,19 @@ def _is_valid_type( # Ignore second item # and _is_valid_type(match.group(3), node.slice.elts[1]) ) + + # This is the default case return ( - isinstance(node, nodes.Subscript) - and _is_valid_type(match.group(1), node.value) + _is_valid_type(match.group(1), node.value) and isinstance(node.slice, nodes.Tuple) - and _is_valid_type(match.group(2), node.slice.elts[0]) - and _is_valid_type(match.group(3), node.slice.elts[1]) + and all( + _is_valid_type(match.group(n + 2), node.slice.elts[n]) + for n in range(len(node.slice.elts)) + ) ) - # Special case for xxx[yyy]` - if match := _TYPE_HINT_MATCHERS["x_of_y"].match(expected_type): + # Special case for xxx[yyy] + if match := _TYPE_HINT_MATCHERS["x_of_y_1"].match(expected_type): return ( isinstance(node, nodes.Subscript) and _is_valid_type(match.group(1), node.value) diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index df10c7514c6..e549d21fe0f 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -40,53 +40,34 @@ def test_regex_get_module_platform( @pytest.mark.parametrize( - ("string", "expected_x", "expected_y", "expected_z", "expected_a"), + ("string", "expected_count", "expected_items"), [ - ("list[dict[str, str]]", "list", "dict", "str", "str"), - ("list[dict[str, Any]]", "list", "dict", "str", "Any"), + ("Callable[..., None]", 2, ("Callable", "...", "None")), + ("Callable[..., Awaitable[None]]", 2, ("Callable", "...", "Awaitable[None]")), + ("tuple[int, int, int, int]", 4, ("tuple", "int", "int", "int", "int")), + ( + "tuple[int, int, int, int, int]", + 5, + ("tuple", "int", "int", "int", "int", "int"), + ), + ("Awaitable[None]", 1, ("Awaitable", "None")), + ("list[dict[str, str]]", 1, ("list", "dict[str, str]")), + ("list[dict[str, Any]]", 1, ("list", "dict[str, Any]")), ], ) -def test_regex_x_of_y_of_z_comma_a( +def test_regex_x_of_y_i( hass_enforce_type_hints: ModuleType, string: str, - expected_x: str, - expected_y: str, - expected_z: str, - expected_a: str, + expected_count: int, + expected_items: tuple[str, ...], ) -> None: - """Test x_of_y_of_z_comma_a regexes.""" + """Test x_of_y_i regexes.""" matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS - assert (match := matchers["x_of_y_of_z_comma_a"].match(string)) + assert (match := matchers[f"x_of_y_{expected_count}"].match(string)) assert match.group(0) == string - assert match.group(1) == expected_x - assert match.group(2) == expected_y - assert match.group(3) == expected_z - assert match.group(4) == expected_a - - -@pytest.mark.parametrize( - ("string", "expected_x", "expected_y", "expected_z"), - [ - ("Callable[..., None]", "Callable", "...", "None"), - ("Callable[..., Awaitable[None]]", "Callable", "...", "Awaitable[None]"), - ], -) -def test_regex_x_of_y_comma_z( - hass_enforce_type_hints: ModuleType, - string: str, - expected_x: str, - expected_y: str, - expected_z: str, -) -> None: - """Test x_of_y_comma_z regexes.""" - matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS - - assert (match := matchers["x_of_y_comma_z"].match(string)) - assert match.group(0) == string - assert match.group(1) == expected_x - assert match.group(2) == expected_y - assert match.group(3) == expected_z + for index in range(expected_count): + assert match.group(index + 1) == expected_items[index] @pytest.mark.parametrize( @@ -743,3 +724,105 @@ def test_valid_mapping_return_type( with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_valid_long_tuple( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check invalid entity properties are ignored by default.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, _, _ = astroid.extract_node( + """ + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class LightEntity(ToggleEntity): + pass + + class TestLight( #@ + LightEntity + ): + @property + def rgbw_color( #@ + self + ) -> tuple[int, int, int, int]: + pass + + @property + def rgbww_color( #@ + self + ) -> tuple[int, int, int, int, int]: + pass + """, + "homeassistant.components.pylint_test.light", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) + + +def test_invalid_long_tuple( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check invalid entity properties are ignored by default.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, rgbw_node, rgbww_node = astroid.extract_node( + """ + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class LightEntity(ToggleEntity): + pass + + class TestLight( #@ + LightEntity + ): + @property + def rgbw_color( #@ + self + ) -> tuple[int, int, int, int, int]: + pass + + @property + def rgbww_color( #@ + self + ) -> tuple[int, int, int, int, float]: + pass + """, + "homeassistant.components.pylint_test.light", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=rgbw_node, + args=["tuple[int, int, int, int]", None], + line=15, + col_offset=4, + end_line=15, + end_col_offset=18, + ), + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=rgbww_node, + args=["tuple[int, int, int, int, int]", None], + line=21, + col_offset=4, + end_line=21, + end_col_offset=19, + ), + ): + type_hint_checker.visit_classdef(class_node) From 2f375004439208b74fb53b0936ea2f15296110ae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 01:51:46 +0200 Subject: [PATCH 285/820] Update shodan to 1.28.0 (#74850) --- homeassistant/components/shodan/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 7d609a5e9c3..98af966c496 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,7 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": ["shodan==1.27.0"], + "requirements": ["shodan==1.28.0"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling", "loggers": ["shodan"] diff --git a/requirements_all.txt b/requirements_all.txt index 1842c31497f..b8ff50bd853 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2162,7 +2162,7 @@ sharkiq==0.0.1 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.27.0 +shodan==1.28.0 # homeassistant.components.sighthound simplehound==0.3 From d40ad9691685504b30cf02bb4d21b25198c42bd9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 10 Jul 2022 00:27:48 +0000 Subject: [PATCH 286/820] [ci skip] Translation update --- .../components/anthemav/translations/ar.json | 18 ++++++++++++++ .../components/awair/translations/ar.json | 11 +++++++++ .../here_travel_time/translations/ar.json | 12 ++++++++++ .../translations/zh-Hant.json | 7 ++++++ .../components/hive/translations/ar.json | 11 +++++++++ .../lg_soundbar/translations/ar.json | 18 ++++++++++++++ .../components/life360/translations/ar.json | 18 ++++++++++++++ .../components/nextdns/translations/ar.json | 20 ++++++++++++++++ .../components/nina/translations/ar.json | 24 +++++++++++++++++++ .../overkiz/translations/sensor.ar.json | 7 ++++++ .../components/qnap_qsw/translations/ar.json | 12 ++++++++++ .../radiotherm/translations/ar.json | 11 +++++++++ .../sensibo/translations/sensor.ar.json | 7 ++++++ .../simplepush/translations/ar.json | 18 ++++++++++++++ .../soundtouch/translations/ar.json | 21 ++++++++++++++++ 15 files changed, 215 insertions(+) create mode 100644 homeassistant/components/anthemav/translations/ar.json create mode 100644 homeassistant/components/awair/translations/ar.json create mode 100644 homeassistant/components/here_travel_time/translations/ar.json create mode 100644 homeassistant/components/hive/translations/ar.json create mode 100644 homeassistant/components/lg_soundbar/translations/ar.json create mode 100644 homeassistant/components/life360/translations/ar.json create mode 100644 homeassistant/components/nextdns/translations/ar.json create mode 100644 homeassistant/components/nina/translations/ar.json create mode 100644 homeassistant/components/overkiz/translations/sensor.ar.json create mode 100644 homeassistant/components/qnap_qsw/translations/ar.json create mode 100644 homeassistant/components/radiotherm/translations/ar.json create mode 100644 homeassistant/components/sensibo/translations/sensor.ar.json create mode 100644 homeassistant/components/simplepush/translations/ar.json create mode 100644 homeassistant/components/soundtouch/translations/ar.json diff --git a/homeassistant/components/anthemav/translations/ar.json b/homeassistant/components/anthemav/translations/ar.json new file mode 100644 index 00000000000..558784a02e4 --- /dev/null +++ b/homeassistant/components/anthemav/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0647\u0627\u0632 \u062a\u0645 \u062a\u0647\u064a\u0623\u062a\u0647 \u0645\u0633\u0628\u0642\u0627" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "host": "\u0645\u0636\u064a\u0641", + "port": "\u0645\u0646\u0641\u0630" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ar.json b/homeassistant/components/awair/translations/ar.json new file mode 100644 index 00000000000..3f25b63e7b9 --- /dev/null +++ b/homeassistant/components/awair/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "email": "\u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/ar.json b/homeassistant/components/here_travel_time/translations/ar.json new file mode 100644 index 00000000000..adea555a76a --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/ar.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0645\u0648\u0642\u0639 \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "origin_entity": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0643\u064a\u0627\u0646" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/zh-Hant.json b/homeassistant/components/here_travel_time/translations/zh-Hant.json index 53d5eae18fd..4476294b661 100644 --- a/homeassistant/components/here_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/here_travel_time/translations/zh-Hant.json @@ -39,6 +39,13 @@ }, "title": "\u9078\u64c7\u51fa\u767c\u5730" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u4f7f\u7528\u5730\u5716\u5ea7\u6a19", + "origin_entity": "\u4f7f\u7528\u70ba\u5be6\u9ad4" + }, + "title": "\u9078\u64c7\u51fa\u767c\u5730" + }, "user": { "data": { "api_key": "API \u91d1\u9470", diff --git a/homeassistant/components/hive/translations/ar.json b/homeassistant/components/hive/translations/ar.json new file mode 100644 index 00000000000..fd198262e7e --- /dev/null +++ b/homeassistant/components/hive/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "configuration": { + "data": { + "device_name": "\u0627\u0633\u0645 \u0627\u0644\u062c\u0647\u0627\u0632" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ar.json b/homeassistant/components/lg_soundbar/translations/ar.json new file mode 100644 index 00000000000..3fc833f41f1 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062e\u062f\u0645\u0629 \u062a\u0645 \u062a\u0647\u064a\u0623\u062a\u0647\u0627 \u0645\u0633\u0628\u0642\u0627", + "existing_instance_updated": "\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u062d\u0627\u0644\u064a" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ar.json b/homeassistant/components/life360/translations/ar.json new file mode 100644 index 00000000000..32dbd371473 --- /dev/null +++ b/homeassistant/components/life360/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062d\u0633\u0627\u0628 \u062a\u0645 \u0625\u0639\u062f\u0627\u062f\u0647 \u0645\u0633\u0628\u0642\u0627", + "reauth_successful": "\u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0648\u062b\u064a\u0642 \u062a\u0645\u062a \u0628\u0646\u062c\u0627\u062d" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + } + }, + "options": { + "step": { + "init": { + "title": "\u062e\u064a\u0627\u0631\u0627\u062a \u0627\u0644\u062d\u0633\u0627\u0628" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ar.json b/homeassistant/components/nextdns/translations/ar.json new file mode 100644 index 00000000000..965e46a92e7 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ar.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644", + "unknown": "\u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" + }, + "step": { + "profiles": { + "data": { + "profile": "\u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u062c\u0627\u0631\u064a \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u062e\u0627\u062f\u0645" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ar.json b/homeassistant/components/nina/translations/ar.json new file mode 100644 index 00000000000..d0c2b4eccbc --- /dev/null +++ b/homeassistant/components/nina/translations/ar.json @@ -0,0 +1,24 @@ +{ + "options": { + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644", + "no_selection": "\u0645\u0646 \u0641\u0636\u0644\u0643 \u0627\u062e\u062a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0623\u0642\u0644 \u0645\u062f\u064a\u0646\u0629/\u0645\u0642\u0627\u0637\u0639\u0629 \u0648\u0627\u062d\u062f\u0629", + "unknown": "\u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629/\u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (A-D)", + "_e_to_h": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629/\u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (E-H)", + "_i_to_l": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629/\u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (I-L)", + "_m_to_q": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629 / \u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (MQ)", + "_r_to_u": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629 / \u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (RU)", + "_v_to_z": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629 / \u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (VZ)", + "corona_filter": "\u0623\u0632\u0644 \u062a\u062d\u0630\u064a\u0631\u0627\u062a \u0643\u0648\u0631\u0648\u0646\u0627", + "slots": "\u0627\u0644\u062a\u062d\u0630\u064a\u0631\u0627\u062a \u0627\u0644\u0642\u0635\u0648\u0649 \u0644\u0643\u0644 \u0645\u062f\u064a\u0646\u0629/\u0645\u062d\u0627\u0641\u0638\u0629" + }, + "title": "\u0627\u0644\u062e\u064a\u0627\u0631\u0627\u062a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.ar.json b/homeassistant/components/overkiz/translations/sensor.ar.json new file mode 100644 index 00000000000..a68781b47cd --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.ar.json @@ -0,0 +1,7 @@ +{ + "state": { + "overkiz__three_way_handle_direction": { + "tilt": "\u0625\u0645\u0627\u0644\u0629" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/ar.json b/homeassistant/components/qnap_qsw/translations/ar.json new file mode 100644 index 00000000000..07980293874 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/ar.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "discovered_connection": { + "data": { + "password": "\u0627\u0644\u0631\u0642\u0645 \u0627\u0644\u0633\u0631\u064a", + "username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ar.json b/homeassistant/components/radiotherm/translations/ar.json new file mode 100644 index 00000000000..217c499dd98 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.ar.json b/homeassistant/components/sensibo/translations/sensor.ar.json new file mode 100644 index 00000000000..b8d510999fd --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.ar.json @@ -0,0 +1,7 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u0637\u0628\u064a\u0639\u064a\u0627" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ar.json b/homeassistant/components/simplepush/translations/ar.json new file mode 100644 index 00000000000..81b5a92d394 --- /dev/null +++ b/homeassistant/components/simplepush/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0647\u0627\u0632 \u062a\u0645 \u062a\u0647\u064a\u0626\u062a\u0647 \u0645\u0646 \u0642\u0628\u0644 " + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "device_key": "\u0627\u0644\u0645\u0641\u062a\u0627\u062d \u0627\u0644\u062e\u0627\u0635 \u0628\u062c\u0647\u0627\u0632\u0643", + "name": "\u0627\u0644\u0627\u0633\u0645" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ar.json b/homeassistant/components/soundtouch/translations/ar.json new file mode 100644 index 00000000000..22fdd032123 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ar.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0647\u0627\u0632 \u062a\u0645 \u0625\u0639\u062f\u0627\u062f\u0647 \u0645\u0633\u0628\u0642\u0627" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641" + } + }, + "zeroconf_confirm": { + "description": "\u0623\u0646\u062a \u0639\u0644\u0649 \u0648\u0634\u0643 \u0627\u0636\u0627\u0641\u0629 \u062c\u0647\u0627\u0632 SoundTouch \u0630\u0627\u062a \u0627\u0644\u0627\u0633\u0645 {name} \u0625\u0644\u0649 Home Assistant", + "title": "\u062c\u0627\u0631\u064a \u0625\u0636\u0627\u0641\u0629 \u062c\u0647\u0627\u0632 Bose SoundTouch" + } + } + } +} \ No newline at end of file From 80727ff952b929fbd30781ee0e2af8d7b5705a63 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 03:30:22 +0200 Subject: [PATCH 287/820] Update pyudev to 0.23.2 (#74859) --- homeassistant/components/usb/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/usb/manifest.json b/homeassistant/components/usb/manifest.json index 87b81965c25..22dca558379 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -2,7 +2,7 @@ "domain": "usb", "name": "USB Discovery", "documentation": "https://www.home-assistant.io/integrations/usb", - "requirements": ["pyudev==0.22.0", "pyserial==3.5"], + "requirements": ["pyudev==0.23.2", "pyserial==3.5"], "codeowners": ["@bdraco"], "dependencies": ["websocket_api"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8f0e2087c14..d53db9a7464 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ pillow==9.2.0 pip>=21.0,<22.2 pyserial==3.5 python-slugify==4.0.1 -pyudev==0.22.0 +pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 scapy==2.4.5 diff --git a/requirements_all.txt b/requirements_all.txt index b8ff50bd853..493a5fa97d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytradfri[async]==9.0.0 pytrafikverket==0.2.0.1 # homeassistant.components.usb -pyudev==0.22.0 +pyudev==0.23.2 # homeassistant.components.unifiprotect pyunifiprotect==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 269ce43c0a8..aa0bb68a8b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1334,7 +1334,7 @@ pytradfri[async]==9.0.0 pytrafikverket==0.2.0.1 # homeassistant.components.usb -pyudev==0.22.0 +pyudev==0.23.2 # homeassistant.components.unifiprotect pyunifiprotect==4.0.9 From f7aea76e73744b02519469e4c0c2609f23ccdddd Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sun, 10 Jul 2022 06:33:54 -0400 Subject: [PATCH 288/820] Bump pymazda to 0.3.6 (#74863) --- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index a18b4406355..acf5282689f 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.3.3"], + "requirements": ["pymazda==0.3.6"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 493a5fa97d9..334ea849a0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1643,7 +1643,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.3 +pymazda==0.3.6 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa0bb68a8b1..ac5f3f7148d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1119,7 +1119,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.3 +pymazda==0.3.6 # homeassistant.components.melcloud pymelcloud==2.5.6 From cb4d2d1b268cbfa6bafd547671c621d625b88336 Mon Sep 17 00:00:00 2001 From: David Straub Date: Sun, 10 Jul 2022 12:49:18 +0200 Subject: [PATCH 289/820] Bump pysml to 0.0.8 (fixes #74382) (#74875) --- homeassistant/components/edl21/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json index 4cffabe87fc..cac35a10152 100644 --- a/homeassistant/components/edl21/manifest.json +++ b/homeassistant/components/edl21/manifest.json @@ -2,7 +2,7 @@ "domain": "edl21", "name": "EDL21", "documentation": "https://www.home-assistant.io/integrations/edl21", - "requirements": ["pysml==0.0.7"], + "requirements": ["pysml==0.0.8"], "codeowners": ["@mtdcr"], "iot_class": "local_push", "loggers": ["sml"] diff --git a/requirements_all.txt b/requirements_all.txt index 334ea849a0f..723a6c4ed5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1838,7 +1838,7 @@ pysmartthings==0.7.6 pysmarty==0.8 # homeassistant.components.edl21 -pysml==0.0.7 +pysml==0.0.8 # homeassistant.components.snmp pysnmplib==5.0.15 From 3429a75cc5d9be8560dfef5c2619d21273e2bffe Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 10 Jul 2022 05:51:40 -0500 Subject: [PATCH 290/820] Bump rokuecp to 0.17.0 (#74862) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 05fe0e1b260..910516b93e8 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.16.0"], + "requirements": ["rokuecp==0.17.0"], "homekit": { "models": ["3820X", "3810X", "4660X", "7820X", "C105X", "C135X"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 723a6c4ed5b..c25a6dd40eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2095,7 +2095,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.16.0 +rokuecp==0.17.0 # homeassistant.components.roomba roombapy==1.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac5f3f7148d..832bfa1bdee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1397,7 +1397,7 @@ rflink==0.0.63 ring_doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.16.0 +rokuecp==0.17.0 # homeassistant.components.roomba roombapy==1.6.5 From 70ceccb06a545711445ac33b62b8a6c84d44b146 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 14:21:14 +0200 Subject: [PATCH 291/820] Update respx to 0.19.2 (#74878) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 113794bda1c..822fb02c6e0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -25,7 +25,7 @@ pytest-timeout==2.1.0 pytest-xdist==2.5.0 pytest==7.1.2 requests_mock==1.9.2 -respx==0.19.0 +respx==0.19.2 stdlib-list==0.7.0 tomli==2.0.1;python_version<"3.11" tqdm==4.49.0 From 52130b227e349d5cef221ee101f85d4368bee7c6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 14:55:43 +0200 Subject: [PATCH 292/820] Update flake8-comprehensions to 3.10.0 (#74882) --- .pre-commit-config.yaml | 2 +- homeassistant/util/color.py | 14 ++++++-------- requirements_test_pre_commit.txt | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5da3a6b21ff..fd2d5ac7e20 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - pyflakes==2.4.0 - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - - flake8-comprehensions==3.8.0 + - flake8-comprehensions==3.10.0 - flake8-noqa==1.2.1 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 448b417cc97..494ee04546c 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -294,22 +294,20 @@ def color_xy_brightness_to_RGB( b = X * 0.051713 - Y * 0.121364 + Z * 1.011530 # Apply reverse gamma correction. - r, g, b = map( - lambda x: (12.92 * x) # type: ignore[no-any-return] - if (x <= 0.0031308) - else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055), - [r, g, b], + r, g, b = ( + 12.92 * x if (x <= 0.0031308) else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055) + for x in (r, g, b) ) # Bring all negative components to zero. - r, g, b = map(lambda x: max(0, x), [r, g, b]) + r, g, b = (max(0, x) for x in (r, g, b)) # If one component is greater than 1, weight components by that value. max_component = max(r, g, b) if max_component > 1: - r, g, b = map(lambda x: x / max_component, [r, g, b]) + r, g, b = (x / max_component for x in (r, g, b)) - ir, ig, ib = map(lambda x: int(x * 255), [r, g, b]) + ir, ig, ib = (int(x * 255) for x in (r, g, b)) return (ir, ig, ib) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 23d675afaf1..c229bc51731 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -3,7 +3,7 @@ bandit==1.7.4 black==22.6.0 codespell==2.1.0 -flake8-comprehensions==3.8.0 +flake8-comprehensions==3.10.0 flake8-docstrings==1.6.0 flake8-noqa==1.2.1 flake8==4.0.1 From 6f9fcdff99fa26c3254a5d5e27f1cb0c7e049d01 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Jul 2022 10:04:57 -0700 Subject: [PATCH 293/820] Improve calendar error handling to match best practices (#74891) --- homeassistant/components/calendar/__init__.py | 12 +++++-- homeassistant/components/google/calendar.py | 3 +- tests/components/calendar/test_init.py | 34 +++++++++++++++++-- tests/components/google/test_calendar.py | 5 +-- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 4623f490302..54da2a1cb02 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -14,6 +14,7 @@ from homeassistant.components import frontend, http from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -336,9 +337,14 @@ class CalendarEventView(http.HomeAssistantView): if not isinstance(entity, CalendarEntity): return web.Response(status=HTTPStatus.BAD_REQUEST) - calendar_event_list = await entity.async_get_events( - request.app["hass"], start_date, end_date - ) + try: + calendar_event_list = await entity.async_get_events( + request.app["hass"], start_date, end_date + ) + except HomeAssistantError as err: + return self.json_message( + f"Error reading events: {err}", HTTPStatus.INTERNAL_SERVER_ERROR + ) return self.json( [ { diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 534b1cbdef3..79e4b2da114 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -282,8 +282,7 @@ class GoogleCalendarEntity(CalendarEntity): async for result_page in result: result_items.extend(result_page.items) except ApiException as err: - _LOGGER.error("Unable to connect to Google: %s", err) - return [] + raise HomeAssistantError(str(err)) from err return [ _get_calendar_event(event) for event in filter(self._event_filter, result_items) diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 0a91f58b0b2..97bfd89f465 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -1,8 +1,10 @@ """The tests for the calendar component.""" from datetime import timedelta from http import HTTPStatus +from unittest.mock import patch from homeassistant.bootstrap import async_setup_component +from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util @@ -11,8 +13,6 @@ async def test_events_http_api(hass, hass_client): await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) await hass.async_block_till_done() client = await hass_client() - response = await client.get("/api/calendars/calendar.calendar_2") - assert response.status == HTTPStatus.BAD_REQUEST start = dt_util.now() end = start + timedelta(days=1) response = await client.get( @@ -25,6 +25,36 @@ async def test_events_http_api(hass, hass_client): assert events[0]["summary"] == "Future Event" +async def test_events_http_api_missing_fields(hass, hass_client): + """Test the calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + client = await hass_client() + response = await client.get("/api/calendars/calendar.calendar_2") + assert response.status == HTTPStatus.BAD_REQUEST + + +async def test_events_http_api_error(hass, hass_client): + """Test the calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + client = await hass_client() + start = dt_util.now() + end = start + timedelta(days=1) + + with patch( + "homeassistant.components.demo.calendar.DemoCalendar.async_get_events", + side_effect=HomeAssistantError("Failure"), + ): + response = await client.get( + "/api/calendars/calendar.calendar_1?start={}&end={}".format( + start.isoformat(), end.isoformat() + ) + ) + assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR + assert await response.json() == {"message": "Error reading events: Failure"} + + async def test_calendars_http_api(hass, hass_client): """Test the calendar demo view.""" await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index f92519e2553..e49eb0c2e01 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -423,10 +423,7 @@ async def test_http_event_api_failure( mock_events_list({}, exc=ClientError()) response = await client.get(upcoming_event_url()) - assert response.status == HTTPStatus.OK - # A failure to talk to the server results in an empty list of events - events = await response.json() - assert events == [] + assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR @pytest.mark.freeze_time("2022-03-27 12:05:00+00:00") From 240a83239a0f2aaaf14372d52699ca28457cbf9c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 10 Jul 2022 13:35:45 -0400 Subject: [PATCH 294/820] Correctly handle device triggers for missing ZHA devices (#74894) --- homeassistant/components/zha/core/helpers.py | 2 +- tests/components/zha/test_device_trigger.py | 38 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 390ef290dc2..b60f61b1e8e 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -170,7 +170,7 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) if not registry_device: - raise ValueError(f"Device id `{device_id}` not found in registry.") + raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] ieee = zigpy.types.EUI64.convert(ieee_address) diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 1f5fa467a93..8e19fe5b637 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -370,3 +370,41 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): ) await hass.async_block_till_done() assert "Invalid config for [automation]" in caplog.text + + +async def test_exception_no_device(hass, mock_devices, calls, caplog): + """Test for exception on event triggers firing.""" + + zigpy_device, zha_device = mock_devices + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": "no_such_device_id", + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert "Invalid config for [automation]" in caplog.text From edaafadde0d31c400524276fabe8bf78ec314911 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 10 Jul 2022 13:46:22 -0400 Subject: [PATCH 295/820] Remove ZHA device storage (#74837) * Remove ZHA device storage * remove storage file if it exists --- homeassistant/components/zha/__init__.py | 12 +- homeassistant/components/zha/core/device.py | 8 - homeassistant/components/zha/core/gateway.py | 27 +--- homeassistant/components/zha/core/store.py | 146 ------------------- tests/components/zha/conftest.py | 17 +-- tests/components/zha/test_gateway.py | 64 +------- 6 files changed, 15 insertions(+), 259 deletions(-) delete mode 100644 homeassistant/components/zha/core/store.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 0e11d992a25..80956117ff9 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,6 +1,7 @@ """Support for Zigbee Home Automation devices.""" import asyncio import logging +import os import voluptuous as vol from zhaquirks import setup as setup_quirks @@ -12,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType from . import api @@ -98,6 +100,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if config.get(CONF_ENABLE_QUIRKS, True): setup_quirks(config) + # temporary code to remove the zha storage file from disk. this will be removed in 2022.10.0 + storage_path = hass.config.path(STORAGE_DIR, "zha.storage") + if os.path.isfile(storage_path): + _LOGGER.debug("removing ZHA storage file") + await hass.async_add_executor_job(os.remove, storage_path) + else: + _LOGGER.debug("ZHA storage file does not exist or was already removed") + zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() @@ -124,7 +134,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Handle shutdown tasks.""" zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY] await zha_gateway.shutdown() - await zha_gateway.async_update_device_storage() zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown @@ -137,7 +146,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload ZHA config entry.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] await zha_gateway.shutdown() - await zha_gateway.async_update_device_storage() GROUP_PROBE.cleanup() api.async_unload_api(hass) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e83c0afbceb..afd4f647ecb 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -470,8 +470,6 @@ class ZHADevice(LogMixin): self.debug("started configuration") await self._channels.async_configure() self.debug("completed configuration") - entry = self.gateway.zha_storage.async_create_or_update_device(self) - self.debug("stored in registry: %s", entry) if ( should_identify @@ -496,12 +494,6 @@ class ZHADevice(LogMixin): for unsubscribe in self.unsubs: unsubscribe() - @callback - def async_update_last_seen(self, last_seen: float | None) -> None: - """Set last seen on the zigpy device.""" - if self._zigpy_device.last_seen is None and last_seen is not None: - self._zigpy_device.last_seen = last_seen - @property def zha_device_info(self) -> dict[str, Any]: """Get ZHA device information.""" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 099abfe5e88..7782d8ef7fd 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -27,7 +27,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from . import discovery @@ -81,14 +80,12 @@ from .const import ( from .device import DeviceStatus, ZHADevice from .group import GroupMember, ZHAGroup from .registries import GROUP_ENTITY_DOMAINS -from .store import async_get_registry if TYPE_CHECKING: from logging import Filter, LogRecord from ..entity import ZhaEntity from .channels.base import ZigbeeChannel - from .store import ZhaStorage _LogFilterType = Union[Filter, Callable[[LogRecord], int]] @@ -118,7 +115,6 @@ class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" # -- Set in async_initialize -- - zha_storage: ZhaStorage ha_device_registry: dr.DeviceRegistry ha_entity_registry: er.EntityRegistry application_controller: ControllerApplication @@ -150,7 +146,6 @@ class ZHAGateway: discovery.PROBE.initialize(self._hass) discovery.GROUP_PROBE.initialize(self._hass) - self.zha_storage = await async_get_registry(self._hass) self.ha_device_registry = dr.async_get(self._hass) self.ha_entity_registry = er.async_get(self._hass) @@ -196,10 +191,9 @@ class ZHAGateway: zha_device = self._async_get_or_create_device(zigpy_device, restored=True) if zha_device.ieee == self.application_controller.ieee: self.coordinator_zha_device = zha_device - zha_dev_entry = self.zha_storage.devices.get(str(zigpy_device.ieee)) delta_msg = "not known" - if zha_dev_entry and zha_dev_entry.last_seen is not None: - delta = round(time.time() - zha_dev_entry.last_seen) + if zha_device.last_seen is not None: + delta = round(time.time() - zha_device.last_seen) zha_device.available = delta < zha_device.consider_unavailable_time delta_msg = f"{str(timedelta(seconds=delta))} ago" _LOGGER.debug( @@ -210,13 +204,6 @@ class ZHAGateway: delta_msg, zha_device.consider_unavailable_time, ) - # update the last seen time for devices every 10 minutes to avoid thrashing - # writes and shutdown issues where storage isn't updated - self._unsubs.append( - async_track_time_interval( - self._hass, self.async_update_device_storage, timedelta(minutes=10) - ) - ) @callback def async_load_groups(self) -> None: @@ -526,8 +513,6 @@ class ZHAGateway: model=zha_device.model, ) zha_device.set_device_id(device_registry_device.id) - entry = self.zha_storage.async_get_or_create_device(zha_device) - zha_device.async_update_last_seen(entry.last_seen) return zha_device @callback @@ -550,17 +535,9 @@ class ZHAGateway: if device.status is DeviceStatus.INITIALIZED: device.update_available(available) - async def async_update_device_storage(self, *_: Any) -> None: - """Update the devices in the store.""" - for device in self.devices.values(): - self.zha_storage.async_update_device(device) - async def async_device_initialized(self, device: zigpy.device.Device) -> None: """Handle device joined and basic information discovered (async).""" zha_device = self._async_get_or_create_device(device) - # This is an active device so set a last seen if it is none - if zha_device.last_seen is None: - zha_device.async_update_last_seen(time.time()) _LOGGER.debug( "device - %s:%s entering async_device_initialized - is_new_join: %s", device.nwk, diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py deleted file mode 100644 index 0b7564fe815..00000000000 --- a/homeassistant/components/zha/core/store.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Data storage helper for ZHA.""" -from __future__ import annotations - -from collections import OrderedDict -from collections.abc import MutableMapping -import datetime -import time -from typing import TYPE_CHECKING, Any, cast - -import attr - -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.storage import Store -from homeassistant.loader import bind_hass - -if TYPE_CHECKING: - from .device import ZHADevice - -DATA_REGISTRY = "zha_storage" - -STORAGE_KEY = "zha.storage" -STORAGE_VERSION = 1 -SAVE_DELAY = 10 -TOMBSTONE_LIFETIME = datetime.timedelta(days=60).total_seconds() - - -@attr.s(slots=True, frozen=True) -class ZhaDeviceEntry: - """Zha Device storage Entry.""" - - name: str | None = attr.ib(default=None) - ieee: str | None = attr.ib(default=None) - last_seen: float | None = attr.ib(default=None) - - -class ZhaStorage: - """Class to hold a registry of zha devices.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the zha device storage.""" - self.hass: HomeAssistant = hass - self.devices: MutableMapping[str, ZhaDeviceEntry] = {} - self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) - - @callback - def async_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create a new ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - device_entry: ZhaDeviceEntry = ZhaDeviceEntry( - name=device.name, ieee=ieee_str, last_seen=device.last_seen - ) - self.devices[ieee_str] = device_entry - self.async_schedule_save() - return device_entry - - @callback - def async_get_or_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create a new ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - if ieee_str in self.devices: - return self.devices[ieee_str] - return self.async_create_device(device) - - @callback - def async_create_or_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create or update a ZhaDeviceEntry.""" - if str(device.ieee) in self.devices: - return self.async_update_device(device) - return self.async_create_device(device) - - @callback - def async_delete_device(self, device: ZHADevice) -> None: - """Delete ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - if ieee_str in self.devices: - del self.devices[ieee_str] - self.async_schedule_save() - - @callback - def async_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Update name of ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - old = self.devices[ieee_str] - - if device.last_seen is None: - return old - - changes = {} - changes["last_seen"] = device.last_seen - - new = self.devices[ieee_str] = attr.evolve(old, **changes) - self.async_schedule_save() - return new - - async def async_load(self) -> None: - """Load the registry of zha device entries.""" - data = await self._store.async_load() - - devices: OrderedDict[str, ZhaDeviceEntry] = OrderedDict() - - if data is not None: - for device in data["devices"]: - devices[device["ieee"]] = ZhaDeviceEntry( - name=device["name"], - ieee=device["ieee"], - last_seen=device.get("last_seen"), - ) - - self.devices = devices - - @callback - def async_schedule_save(self) -> None: - """Schedule saving the registry of zha devices.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - - async def async_save(self) -> None: - """Save the registry of zha devices.""" - await self._store.async_save(self._data_to_save()) - - @callback - def _data_to_save(self) -> dict: - """Return data for the registry of zha devices to store in a file.""" - data = {} - - data["devices"] = [ - {"name": entry.name, "ieee": entry.ieee, "last_seen": entry.last_seen} - for entry in self.devices.values() - if entry.last_seen and (time.time() - entry.last_seen) < TOMBSTONE_LIFETIME - ] - - return data - - -@bind_hass -async def async_get_registry(hass: HomeAssistant) -> ZhaStorage: - """Return zha device storage instance.""" - if (task := hass.data.get(DATA_REGISTRY)) is None: - - async def _load_reg() -> ZhaStorage: - registry = ZhaStorage(hass) - await registry.async_load() - return registry - - task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg()) - - return cast(ZhaStorage, await task) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 482a11b95de..d9223027668 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -15,7 +15,6 @@ from zigpy.state import State import zigpy.types import zigpy.zdo.types as zdo_t -from homeassistant.components.zha import DOMAIN import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.device as zha_core_device from homeassistant.setup import async_setup_component @@ -188,26 +187,14 @@ def zha_device_joined(hass, setup_zha): @pytest.fixture -def zha_device_restored(hass, zigpy_app_controller, setup_zha, hass_storage): +def zha_device_restored(hass, zigpy_app_controller, setup_zha): """Return a restored ZHA device.""" async def _zha_device(zigpy_dev, last_seen=None): zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev if last_seen is not None: - hass_storage[f"{DOMAIN}.storage"] = { - "key": f"{DOMAIN}.storage", - "version": 1, - "data": { - "devices": [ - { - "ieee": str(zigpy_dev.ieee), - "last_seen": last_seen, - "name": f"{zigpy_dev.manufacturer} {zigpy_dev.model}", - } - ], - }, - } + zigpy_dev.last_seen = last_seen await setup_zha() zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index b19c98548ce..3c8c3e78c0e 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,7 +1,5 @@ """Test ZHA Gateway.""" import asyncio -import math -import time from unittest.mock import patch import pytest @@ -10,10 +8,9 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.core.store import TOMBSTONE_LIFETIME from homeassistant.const import Platform -from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway +from .common import async_find_group_entity_id, get_zha_gateway from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @@ -214,62 +211,3 @@ async def test_gateway_create_group_with_id(hass, device_light_1, coordinator): assert len(zha_group.members) == 1 assert zha_group.members[0].device is device_light_1 assert zha_group.group_id == 0x1234 - - -async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): - """Test saving data after a delay.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - await async_enable_traffic(hass, [zha_dev_basic]) - - assert zha_dev_basic.last_seen is not None - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - assert zha_dev_basic.last_seen is not None - last_seen = zha_dev_basic.last_seen - - # test that we can't set None as last seen any more - zha_dev_basic.async_update_last_seen(None) - assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - # test that we won't put None in storage - zigpy_dev_basic.last_seen = None - assert zha_dev_basic.last_seen is None - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) - - # test that we can still set a good last_seen - last_seen = time.time() - zha_dev_basic.async_update_last_seen(last_seen) - assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - # test that we still put good values in storage - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) - - -async def test_cleaning_up_storage(hass, zigpy_dev_basic, zha_dev_basic, hass_storage): - """Test cleaning up zha storage and remove stale devices.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - await async_enable_traffic(hass, [zha_dev_basic]) - - assert zha_dev_basic.last_seen is not None - await zha_gateway.zha_storage.async_save() - await hass.async_block_till_done() - - assert hass_storage["zha.storage"]["data"]["devices"] - device = hass_storage["zha.storage"]["data"]["devices"][0] - assert device["ieee"] == str(zha_dev_basic.ieee) - - zha_dev_basic.device.last_seen = time.time() - TOMBSTONE_LIFETIME - 1 - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - await zha_gateway.zha_storage.async_save() - await hass.async_block_till_done() - assert not hass_storage["zha.storage"]["data"]["devices"] From c9aa3c112aef05a147ee36f1df576bf424cfdf17 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 20:05:24 +0200 Subject: [PATCH 296/820] Migrate GitHub to new entity naming style (#74903) --- homeassistant/components/github/sensor.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 8dff4b04b01..d17f762fedd 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -89,7 +89,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="pulls_count", - name="Pull Requests", + name="Pull requests", native_unit_of_measurement="Pull Requests", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -97,7 +97,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_commit", - name="Latest Commit", + name="Latest commit", value_fn=lambda data: data["default_branch_ref"]["commit"]["message"][:255], attr_fn=lambda data: { "sha": data["default_branch_ref"]["commit"]["sha"], @@ -106,7 +106,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_discussion", - name="Latest Discussion", + name="Latest discussion", avabl_fn=lambda data: data["discussion"]["discussions"], value_fn=lambda data: data["discussion"]["discussions"][0]["title"][:255], attr_fn=lambda data: { @@ -116,7 +116,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_release", - name="Latest Release", + name="Latest release", avabl_fn=lambda data: data["release"] is not None, value_fn=lambda data: data["release"]["name"][:255], attr_fn=lambda data: { @@ -126,7 +126,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_issue", - name="Latest Issue", + name="Latest issue", avabl_fn=lambda data: data["issue"]["issues"], value_fn=lambda data: data["issue"]["issues"][0]["title"][:255], attr_fn=lambda data: { @@ -136,7 +136,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_pull_request", - name="Latest Pull Request", + name="Latest pull request", avabl_fn=lambda data: data["pull_request"]["pull_requests"], value_fn=lambda data: data["pull_request"]["pull_requests"][0]["title"][:255], attr_fn=lambda data: { @@ -146,7 +146,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_tag", - name="Latest Tag", + name="Latest tag", avabl_fn=lambda data: data["refs"]["tags"], value_fn=lambda data: data["refs"]["tags"][0]["name"][:255], attr_fn=lambda data: { @@ -176,6 +176,7 @@ class GitHubSensorEntity(CoordinatorEntity[GitHubDataUpdateCoordinator], SensorE """Defines a GitHub sensor entity.""" _attr_attribution = "Data provided by the GitHub API" + _attr_has_entity_name = True entity_description: GitHubSensorEntityDescription @@ -188,9 +189,6 @@ class GitHubSensorEntity(CoordinatorEntity[GitHubDataUpdateCoordinator], SensorE super().__init__(coordinator=coordinator) self.entity_description = entity_description - self._attr_name = ( - f"{coordinator.data.get('full_name')} {entity_description.name}" - ) self._attr_unique_id = f"{coordinator.data.get('id')}_{entity_description.key}" self._attr_device_info = DeviceInfo( From 59170d3c54bec9ff8ca06dcbe43c5f58d3c77910 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 10 Jul 2022 19:50:54 +0100 Subject: [PATCH 297/820] Prepare homekit_controller for _hap._udp.local. (#74857) * Prepare homekit_controller for _hap._udp.local. --- .../homekit_controller/config_flow.py | 7 ++++++- .../homekit_controller/manifest.json | 4 ++-- homeassistant/generated/zeroconf.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/common.py | 2 +- .../homekit_controller/test_config_flow.py | 19 ++++++++++++++++++- 7 files changed, 34 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 24a04874f20..493dd05a8b2 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -8,6 +8,7 @@ from typing import Any import aiohomekit from aiohomekit.exceptions import AuthenticationError from aiohomekit.model import Accessories, CharacteristicsTypes, ServicesTypes +from aiohomekit.utils import domain_supported, domain_to_name import voluptuous as vol from homeassistant import config_entries @@ -203,8 +204,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): hkid = properties[zeroconf.ATTR_PROPERTIES_ID] normalized_hkid = normalize_hkid(hkid) + # If this aiohomekit doesn't support this particular device, ignore it. + if not domain_supported(discovery_info.name): + return self.async_abort(reason="ignored_model") + model = properties["md"] - name = discovery_info.name.replace("._hap._tcp.local.", "") + name = domain_to_name(discovery_info.name) status_flags = int(properties["sf"]) paired = not status_flags & 0x01 diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 6517b078454..a83d6264603 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,8 +3,8 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.21"], - "zeroconf": ["_hap._tcp.local."], + "requirements": ["aiohomekit==0.7.22"], + "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 3c9d21d1d95..5284eef02a7 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -167,6 +167,11 @@ ZEROCONF = { "name": "*z.wave-me*" } ], + "_hap._udp.local.": [ + { + "domain": "homekit_controller" + } + ], "_homekit._tcp.local.": [ { "domain": "homekit" diff --git a/requirements_all.txt b/requirements_all.txt index c25a6dd40eb..6141bb5f450 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.21 +aiohomekit==0.7.22 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 832bfa1bdee..0c879ca216a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.21 +aiohomekit==0.7.22 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 749bd4b0f07..4cad585d135 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -224,7 +224,7 @@ async def device_config_changed(hass, accessories): host="127.0.0.1", addresses=["127.0.0.1"], hostname="mock_hostname", - name="TestDevice", + name="TestDevice._hap._tcp.local.", port=8080, properties={ "md": "TestDevice", diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 170a2a797d0..7b62c1e9d6d 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -141,7 +141,7 @@ def get_device_discovery_info( result = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", hostname=device.description.name, - name=device.description.name, + name=device.description.name + "._hap._tcp.local.", addresses=["127.0.0.1"], port=8080, properties={ @@ -265,6 +265,23 @@ async def test_pair_already_paired_1(hass, controller): assert result["reason"] == "already_paired" +async def test_unknown_domain_type(hass, controller): + """Test that aiohomekit can reject discoveries it doesn't support.""" + device = setup_mock_accessory(controller) + # Flag device as already paired + discovery_info = get_device_discovery_info(device) + discovery_info.name = "TestDevice._music._tap.local." + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + assert result["type"] == "abort" + assert result["reason"] == "ignored_model" + + async def test_id_missing(hass, controller): """Test id is missing.""" device = setup_mock_accessory(controller) From c241e876efd6c90162aba7d241acb73f74d0759b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 20:55:54 +0200 Subject: [PATCH 298/820] Update url-normalize to 1.4.3 (#74897) --- homeassistant/components/huawei_lte/manifest.json | 2 +- homeassistant/components/syncthru/manifest.json | 2 +- homeassistant/components/zwave_me/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index dd0382d5f55..656c80e5a89 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "huawei-lte-api==1.6.0", "stringcase==1.2.0", - "url-normalize==1.4.1" + "url-normalize==1.4.3" ], "ssdp": [ { diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 4536e703ce9..f0ffe081faf 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,7 +3,7 @@ "name": "Samsung SyncThru Printer", "documentation": "https://www.home-assistant.io/integrations/syncthru", "config_flow": true, - "requirements": ["pysyncthru==0.7.10", "url-normalize==1.4.1"], + "requirements": ["pysyncthru==0.7.10", "url-normalize==1.4.3"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:Printer:1", diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index f69bbc1eea1..04627583d0e 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave.Me", "documentation": "https://www.home-assistant.io/integrations/zwave_me", "iot_class": "local_push", - "requirements": ["zwave_me_ws==0.2.4", "url-normalize==1.4.1"], + "requirements": ["zwave_me_ws==0.2.4", "url-normalize==1.4.3"], "after_dependencies": ["zeroconf"], "zeroconf": [{ "type": "_hap._tcp.local.", "name": "*z.wave-me*" }], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 6141bb5f450..2f1c35c8e83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2372,7 +2372,7 @@ upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru # homeassistant.components.zwave_me -url-normalize==1.4.1 +url-normalize==1.4.3 # homeassistant.components.uscis uscisstatus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c879ca216a..7b24be8441f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1581,7 +1581,7 @@ upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru # homeassistant.components.zwave_me -url-normalize==1.4.1 +url-normalize==1.4.3 # homeassistant.components.uvc uvcclient==0.11.0 From c92936cc7b0f56f483e0c7c82fae58729556aec8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:21:21 -0600 Subject: [PATCH 299/820] Migrate Ambient PWS to new entity naming style (#74743) --- .../components/ambient_station/__init__.py | 4 +- .../ambient_station/binary_sensor.py | 32 ++--- .../components/ambient_station/sensor.py | 112 +++++++++--------- 3 files changed, 74 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index fb4a865a72e..7242c0ba53b 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -198,6 +198,7 @@ class AmbientStation: class AmbientWeatherEntity(Entity): """Define a base Ambient PWS entity.""" + _attr_has_entity_name = True _attr_should_poll = False def __init__( @@ -215,10 +216,9 @@ class AmbientWeatherEntity(Entity): configuration_url=f"https://ambientweather.net/dashboard/{public_device_id}", identifiers={(DOMAIN, mac_address)}, manufacturer="Ambient Weather", - name=station_name, + name=station_name.capitalize(), ) - self._attr_name = f"{station_name}_{description.name}" self._attr_unique_id = f"{mac_address}_{description.key}" self._mac_address = mac_address self.entity_description = description diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 5fecbce3ded..4380e1839f2 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -143,112 +143,112 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), AmbientBinarySensorDescription( key=TYPE_BATTIN, - name="Interior Battery", + name="Interior battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT10, - name="Soil Monitor Battery 10", + name="Battery 10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM1, - name="Soil Monitor Battery 1", + name="Soil monitor battery 1", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM2, - name="Soil Monitor Battery 2", + name="Soil monitor battery 2", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM3, - name="Soil Monitor Battery 3", + name="Soil monitor battery 3", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM4, - name="Soil Monitor Battery 4", + name="Soil monitor battery 4", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM5, - name="Soil Monitor Battery 5", + name="Soil monitor battery 5", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM6, - name="Soil Monitor Battery 6", + name="Soil monitor battery 6", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM7, - name="Soil Monitor Battery 7", + name="Soil monitor battery 7", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM8, - name="Soil Monitor Battery 8", + name="Soil monitor battery 8", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM9, - name="Soil Monitor Battery 9", + name="Soil monitor battery 9", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM10, - name="Soil Monitor Battery 10", + name="Soil monitor battery 10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_CO2, - name="CO2 Battery", + name="CO2 battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_LIGHTNING, - name="Lightning Detector Battery", + name="Lightning detector battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25IN_BATT, - name="PM25 Indoor Battery", + name="PM25 indoor battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25_BATT, - name="PM25 Battery", + name="PM25 battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index c837ef6fdec..2944f33938d 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -113,7 +113,7 @@ TYPE_YEARLYRAININ = "yearlyrainin" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_24HOURRAININ, - name="24 Hr Rain", + name="24 hr rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, @@ -126,74 +126,74 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_AQI_PM25_24H, - name="AQI PM2.5 24h Avg", + name="AQI PM2.5 24h avg", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN, - name="AQI PM2.5 Indoor", + name="AQI PM2.5 indoor", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN_24H, - name="AQI PM2.5 Indoor 24h Avg", + name="AQI PM2.5 indoor 24h avg", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_BAROMABSIN, - name="Abs Pressure", + name="Abs pressure", native_unit_of_measurement=PRESSURE_INHG, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_BAROMRELIN, - name="Rel Pressure", + name="Rel pressure", native_unit_of_measurement=PRESSURE_INHG, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CO2, - name="co2", + name="CO2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_DAILYRAININ, - name="Daily Rain", + name="Daily rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_DEWPOINT, - name="Dew Point", + name="Dew point", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_EVENTRAININ, - name="Event Rain", + name="Event rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_FEELSLIKE, - name="Feels Like", + name="Feels like", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HOURLYRAININ, - name="Hourly Rain Rate", + name="Hourly rain rate", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, @@ -266,60 +266,60 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_HUMIDITYIN, - name="Humidity In", + name="Humidity in", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_LASTRAIN, - name="Last Rain", + name="Last rain", icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_DAY, - name="Lightning Strikes Per Day", + name="Lightning strikes per day", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_HOUR, - name="Lightning Strikes Per Hour", + name="Lightning strikes per hour", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, - name="Max Gust", + name="Max gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_MONTHLYRAININ, - name="Monthly Rain", + name="Monthly rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_PM25_24H, - name="PM25 24h Avg", + name="PM25 24h avg", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, ), SensorEntityDescription( key=TYPE_PM25_IN, - name="PM25 Indoor", + name="PM25 indoor", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_PM25_IN_24H, - name="PM25 Indoor 24h Avg", + name="PM25 indoor 24h avg", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, ), @@ -332,144 +332,144 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_SOILHUM10, - name="Soil Humidity 10", + name="Soil humidity 10", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM1, - name="Soil Humidity 1", + name="Soil humidity 1", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM2, - name="Soil Humidity 2", + name="Soil humidity 2", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM3, - name="Soil Humidity 3", + name="Soil humidity 3", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM4, - name="Soil Humidity 4", + name="Soil humidity 4", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM5, - name="Soil Humidity 5", + name="Soil humidity 5", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM6, - name="Soil Humidity 6", + name="Soil humidity 6", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM7, - name="Soil Humidity 7", + name="Soil humidity 7", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM8, - name="Soil Humidity 8", + name="Soil humidity 8", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM9, - name="Soil Humidity 9", + name="Soil humidity 9", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILTEMP10F, - name="Soil Temp 10", + name="Soil temp 10", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP1F, - name="Soil Temp 1", + name="Soil temp 1", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP2F, - name="Soil Temp 2", + name="Soil temp 2", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP3F, - name="Soil Temp 3", + name="Soil temp 3", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP4F, - name="Soil Temp 4", + name="Soil temp 4", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP5F, - name="Soil Temp 5", + name="Soil temp 5", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP6F, - name="Soil Temp 6", + name="Soil temp 6", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP7F, - name="Soil Temp 7", + name="Soil temp 7", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP8F, - name="Soil Temp 8", + name="Soil temp 8", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP9F, - name="Soil Temp 9", + name="Soil temp 9", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION, - name="Solar Rad", + name="Solar rad", native_unit_of_measurement=IRRADIATION_WATTS_PER_SQUARE_METER, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION_LX, - name="Solar Rad", + name="Solar rad", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, @@ -553,85 +553,85 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_TEMPINF, - name="Inside Temp", + name="Inside temp", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TOTALRAININ, - name="Lifetime Rain", + name="Lifetime rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_UV, - name="UV Index", + name="UV index", native_unit_of_measurement="Index", device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WEEKLYRAININ, - name="Weekly Rain", + name="Weekly rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WINDDIR, - name="Wind Dir", + name="Wind dir", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDDIR_AVG10M, - name="Wind Dir Avg 10m", + name="Wind dir avg 10m", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDDIR_AVG2M, - name="Wind Dir Avg 2m", + name="Wind dir avg 2m", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDGUSTDIR, - name="Gust Dir", + name="Gust dir", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDGUSTMPH, - name="Wind Gust", + name="Wind gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG10M, - name="Wind Avg 10m", + name="Wind avg 10m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG2M, - name="Wind Avg 2m", + name="Wind avg 2m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, ), SensorEntityDescription( key=TYPE_WINDSPEEDMPH, - name="Wind Speed", + name="Wind speed", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_YEARLYRAININ, - name="Yearly Rain", + name="Yearly rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, From a4d5ecb8ec4553b44bfa6fdfa2d93feb3244aa7b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:27:01 -0600 Subject: [PATCH 300/820] Migrate RainMachine to new entity naming style (#74754) --- .../components/rainmachine/__init__.py | 5 +++-- .../components/rainmachine/binary_sensor.py | 18 +++++++++--------- homeassistant/components/rainmachine/sensor.py | 10 +++++----- homeassistant/components/rainmachine/switch.py | 6 ++++-- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 7647a330a30..3feeac7a827 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -401,6 +401,8 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: class RainMachineEntity(CoordinatorEntity): """Define a generic RainMachine entity.""" + _attr_has_entity_name = True + def __init__( self, entry: ConfigEntry, @@ -415,7 +417,7 @@ class RainMachineEntity(CoordinatorEntity): identifiers={(DOMAIN, controller.mac)}, configuration_url=f"https://{entry.data[CONF_IP_ADDRESS]}:{entry.data[CONF_PORT]}", connections={(dr.CONNECTION_NETWORK_MAC, controller.mac)}, - name=str(controller.name), + name=str(controller.name).capitalize(), manufacturer="RainMachine", model=( f"Version {controller.hardware_version} " @@ -424,7 +426,6 @@ class RainMachineEntity(CoordinatorEntity): sw_version=controller.software_version, ) self._attr_extra_state_attributes = {} - self._attr_name = f"{controller.name} {description.name}" self._attr_unique_id = f"{controller.mac}_{description.key}" self._controller = controller self.entity_description = description diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 7a13515db3b..6ba374a28ba 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -43,14 +43,14 @@ class RainMachineBinarySensorDescription( BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_FLOW_SENSOR, - name="Flow Sensor", + name="Flow sensor", icon="mdi:water-pump", api_category=DATA_PROVISION_SETTINGS, data_key="useFlowSensor", ), RainMachineBinarySensorDescription( key=TYPE_FREEZE, - name="Freeze Restrictions", + name="Freeze restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, @@ -58,7 +58,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_FREEZE_PROTECTION, - name="Freeze Protection", + name="Freeze protection", icon="mdi:weather-snowy", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, @@ -66,7 +66,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_HOT_DAYS, - name="Extra Water on Hot Days", + name="Extra water on hot days", icon="mdi:thermometer-lines", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, @@ -74,7 +74,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_HOURLY, - name="Hourly Restrictions", + name="Hourly restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -83,7 +83,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_MONTH, - name="Month Restrictions", + name="Month restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -92,7 +92,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_RAINDELAY, - name="Rain Delay Restrictions", + name="Rain delay restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -101,7 +101,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_RAINSENSOR, - name="Rain Sensor Restrictions", + name="Rain sensor restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -110,7 +110,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_WEEKDAY, - name="Weekday Restrictions", + name="Weekday restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 7550756f8c4..5a5329ad1fa 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -68,7 +68,7 @@ class RainMachineSensorDescriptionUid( SENSOR_DESCRIPTIONS = ( RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_CLICK_M3, - name="Flow Sensor Clicks per Cubic Meter", + name="Flow sensor clicks per cubic meter", icon="mdi:water-pump", native_unit_of_measurement=f"clicks/{VOLUME_CUBIC_METERS}", entity_category=EntityCategory.DIAGNOSTIC, @@ -79,7 +79,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, - name="Flow Sensor Consumed Liters", + name="Flow sensor consumed liters", icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="liter", @@ -90,7 +90,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_START_INDEX, - name="Flow Sensor Start Index", + name="Flow sensor start index", icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="index", @@ -100,7 +100,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, - name="Flow Sensor Clicks", + name="Flow sensor clicks", icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="clicks", @@ -111,7 +111,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FREEZE_TEMP, - name="Freeze Protect Temperature", + name="Freeze protect temperature", icon="mdi:thermometer", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=TEMP_CELSIUS, diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 8d339682305..aa91f529b5b 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -149,6 +149,8 @@ async def async_setup_entry( ("zone", zone_coordinator, RainMachineZone, RainMachineZoneEnabled), ): for uid, data in coordinator.data.items(): + name = data["name"].capitalize() + # Add a switch to start/stop the program or zone: entities.append( switch_class( @@ -157,7 +159,7 @@ async def async_setup_entry( controller, RainMachineSwitchDescription( key=f"{kind}_{uid}", - name=data["name"], + name=name, icon="mdi:water", uid=uid, ), @@ -172,7 +174,7 @@ async def async_setup_entry( controller, RainMachineSwitchDescription( key=f"{kind}_{uid}_enabled", - name=f"{data['name']} Enabled", + name=f"{name} enabled", entity_category=EntityCategory.CONFIG, icon="mdi:cog", uid=uid, From ae4f2a0e3481aadddedc0f5d8b529dd84099f454 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:37:14 -0600 Subject: [PATCH 301/820] Fix incorrect new entity naming for Guardian (#74912) --- homeassistant/components/guardian/__init__.py | 4 ++-- homeassistant/components/guardian/binary_sensor.py | 6 +++--- homeassistant/components/guardian/switch.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index b07d8ec5b3f..3707bf9d2eb 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -409,7 +409,7 @@ class PairedSensorEntity(GuardianEntity): identifiers={(DOMAIN, paired_sensor_uid)}, manufacturer="Elexa", model=coordinator.data["codename"], - name=f"Guardian Paired Sensor {paired_sensor_uid}", + name=f"Guardian paired sensor {paired_sensor_uid}", via_device=(DOMAIN, entry.data[CONF_UID]), ) self._attr_unique_id = f"{paired_sensor_uid}_{description.key}" @@ -436,7 +436,7 @@ class ValveControllerEntity(GuardianEntity): identifiers={(DOMAIN, entry.data[CONF_UID])}, manufacturer="Elexa", model=coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], - name=f"Guardian Valve Controller {entry.data[CONF_UID]}", + name=f"Guardian valve controller {entry.data[CONF_UID]}", ) self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" self.coordinators = coordinators diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 1d7195a8f17..dc9febcfa91 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -32,18 +32,18 @@ SENSOR_KIND_MOVED = "moved" SENSOR_DESCRIPTION_AP_ENABLED = BinarySensorEntityDescription( key=SENSOR_KIND_AP_INFO, - name="Onboard AP Enabled", + name="Onboard AP enabled", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ) SENSOR_DESCRIPTION_LEAK_DETECTED = BinarySensorEntityDescription( key=SENSOR_KIND_LEAK_DETECTED, - name="Leak Detected", + name="Leak detected", device_class=BinarySensorDeviceClass.MOISTURE, ) SENSOR_DESCRIPTION_MOVED = BinarySensorEntityDescription( key=SENSOR_KIND_MOVED, - name="Recently Moved", + name="Recently moved", device_class=BinarySensorDeviceClass.MOVING, entity_category=EntityCategory.DIAGNOSTIC, ) diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 9a4f70fd3d2..485b0a3ffbc 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -25,7 +25,7 @@ SWITCH_KIND_VALVE = "valve" SWITCH_DESCRIPTION_VALVE = SwitchEntityDescription( key=SWITCH_KIND_VALVE, - name="Valve Controller", + name="Valve controller", icon="mdi:water", ) From f95c9d0f02ca9a0f740d212e1d6d6969c65964ad Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:38:00 -0600 Subject: [PATCH 302/820] Migrate AirVisual to new entity naming style (#74753) --- homeassistant/components/airvisual/sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 52d046cef42..e28a11666da 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -62,20 +62,20 @@ SENSOR_KIND_VOC = "voc" GEOGRAPHY_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_LEVEL, - name="Air Pollution Level", + name="Air pollution level", device_class=DEVICE_CLASS_POLLUTANT_LEVEL, icon="mdi:gauge", ), SensorEntityDescription( key=SENSOR_KIND_AQI, - name="Air Quality Index", + name="Air quality index", device_class=SensorDeviceClass.AQI, native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_POLLUTANT, - name="Main Pollutant", + name="Main pollutant", device_class=DEVICE_CLASS_POLLUTANT_LABEL, icon="mdi:chemical-weapon", ), @@ -85,7 +85,7 @@ GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."} NODE_PRO_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_AQI, - name="Air Quality Index", + name="Air quality index", device_class=SensorDeviceClass.AQI, native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, @@ -292,6 +292,8 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to a Node/Pro unit.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, @@ -301,9 +303,6 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Initialize.""" super().__init__(coordinator, entry, description) - self._attr_name = ( - f"{coordinator.data['settings']['node_name']} Node/Pro: {description.name}" - ) self._attr_unique_id = f"{coordinator.data['serial_number']}_{description.key}" @property From 5971ab65493fa3c5d00061359c469ece3f915988 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:40:06 -0600 Subject: [PATCH 303/820] Migrate Flo to new entity naming style (#74744) --- homeassistant/components/flo/binary_sensor.py | 4 +- homeassistant/components/flo/entity.py | 3 +- homeassistant/components/flo/sensor.py | 10 ++-- homeassistant/components/flo/switch.py | 2 +- tests/components/flo/test_binary_sensor.py | 11 +++-- tests/components/flo/test_sensor.py | 49 ++++++++++++------- tests/components/flo/test_services.py | 2 +- tests/components/flo/test_switch.py | 2 +- 8 files changed, 52 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py index c395e6136fe..f5c051d1a70 100644 --- a/homeassistant/components/flo/binary_sensor.py +++ b/homeassistant/components/flo/binary_sensor.py @@ -48,7 +48,7 @@ class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): def __init__(self, device): """Initialize the pending alerts binary sensor.""" - super().__init__("pending_system_alerts", "Pending System Alerts", device) + super().__init__("pending_system_alerts", "Pending system alerts", device) @property def is_on(self): @@ -74,7 +74,7 @@ class FloWaterDetectedBinarySensor(FloEntity, BinarySensorEntity): def __init__(self, device): """Initialize the pending alerts binary sensor.""" - super().__init__("water_detected", "Water Detected", device) + super().__init__("water_detected", "Water detected", device) @property def is_on(self): diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 280f19dc57e..39ad57f5c03 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -14,6 +14,7 @@ class FloEntity(Entity): """A base class for Flo entities.""" _attr_force_update = False + _attr_has_entity_name = True _attr_should_poll = False def __init__( @@ -38,7 +39,7 @@ class FloEntity(Entity): identifiers={(FLO_DOMAIN, self._device.id)}, manufacturer=self._device.manufacturer, model=self._device.model, - name=self._device.device_name, + name=self._device.device_name.capitalize(), sw_version=self._device.firmware_version, ) diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index a5d54386633..e7fbd293bd1 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -22,12 +22,12 @@ from .entity import FloEntity WATER_ICON = "mdi:water" GAUGE_ICON = "mdi:gauge" -NAME_DAILY_USAGE = "Today's Water Usage" -NAME_CURRENT_SYSTEM_MODE = "Current System Mode" -NAME_FLOW_RATE = "Water Flow Rate" -NAME_WATER_TEMPERATURE = "Water Temperature" +NAME_DAILY_USAGE = "Today's water usage" +NAME_CURRENT_SYSTEM_MODE = "Current system mode" +NAME_FLOW_RATE = "Water flow rate" +NAME_WATER_TEMPERATURE = "Water temperature" NAME_AIR_TEMPERATURE = "Temperature" -NAME_WATER_PRESSURE = "Water Pressure" +NAME_WATER_PRESSURE = "Water pressure" NAME_HUMIDITY = "Humidity" NAME_BATTERY = "Battery" diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index 01ab2c9259e..884b76fc64e 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -68,7 +68,7 @@ class FloSwitch(FloEntity, SwitchEntity): def __init__(self, device: FloDeviceDataUpdateCoordinator) -> None: """Initialize the Flo switch.""" - super().__init__("shutoff_valve", "Shutoff Valve", device) + super().__init__("shutoff_valve", "Shutoff valve", device) self._state = self._device.last_known_valve_state == "open" @property diff --git a/tests/components/flo/test_binary_sensor.py b/tests/components/flo/test_binary_sensor.py index b6a8abf727c..36901584cf6 100644 --- a/tests/components/flo/test_binary_sensor.py +++ b/tests/components/flo/test_binary_sensor.py @@ -22,12 +22,17 @@ async def test_binary_sensors(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 - valve_state = hass.states.get("binary_sensor.pending_system_alerts") + valve_state = hass.states.get( + "binary_sensor.smart_water_shutoff_pending_system_alerts" + ) assert valve_state.state == STATE_ON assert valve_state.attributes.get("info") == 0 assert valve_state.attributes.get("warning") == 2 assert valve_state.attributes.get("critical") == 0 - assert valve_state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending System Alerts" + assert ( + valve_state.attributes.get(ATTR_FRIENDLY_NAME) + == "Smart water shutoff Pending system alerts" + ) - detector_state = hass.states.get("binary_sensor.water_detected") + detector_state = hass.states.get("binary_sensor.kitchen_sink_water_detected") assert detector_state.state == STATE_OFF diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index 0a7cf16ecf5..b5439241d33 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -18,48 +18,63 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 # we should have 5 entities for the valve - assert hass.states.get("sensor.current_system_mode").state == "home" - - assert hass.states.get("sensor.today_s_water_usage").state == "3.7" + print(hass.states) assert ( - hass.states.get("sensor.today_s_water_usage").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_current_system_mode").state + == "home" + ) + + assert ( + hass.states.get("sensor.smart_water_shutoff_today_s_water_usage").state == "3.7" + ) + assert ( + hass.states.get("sensor.smart_water_shutoff_today_s_water_usage").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.TOTAL_INCREASING ) - assert hass.states.get("sensor.water_flow_rate").state == "0" + assert hass.states.get("sensor.smart_water_shutoff_water_flow_rate").state == "0" assert ( - hass.states.get("sensor.water_flow_rate").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_water_flow_rate").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.water_pressure").state == "54.2" + assert hass.states.get("sensor.smart_water_shutoff_water_pressure").state == "54.2" assert ( - hass.states.get("sensor.water_pressure").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_water_pressure").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.water_temperature").state == "21" + assert hass.states.get("sensor.smart_water_shutoff_water_temperature").state == "21" assert ( - hass.states.get("sensor.water_temperature").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_water_temperature").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.MEASUREMENT ) # and 3 entities for the detector - assert hass.states.get("sensor.temperature").state == "16" + print(hass.states) + assert hass.states.get("sensor.kitchen_sink_temperature").state == "16" assert ( - hass.states.get("sensor.temperature").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.kitchen_sink_temperature").attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.humidity").state == "43" + assert hass.states.get("sensor.kitchen_sink_humidity").state == "43" assert ( - hass.states.get("sensor.humidity").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.kitchen_sink_humidity").attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.battery").state == "100" + assert hass.states.get("sensor.kitchen_sink_battery").state == "100" assert ( - hass.states.get("sensor.battery").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.kitchen_sink_battery").attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ) @@ -82,7 +97,7 @@ async def test_manual_update_entity( await hass.services.async_call( "homeassistant", "update_entity", - {ATTR_ENTITY_ID: ["sensor.current_system_mode"]}, + {ATTR_ENTITY_ID: ["sensor.smart_water_shutoff_current_system_mode"]}, blocking=True, ) assert aioclient_mock.call_count == call_count + 3 diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py index 720d0596b22..c7e2e070745 100644 --- a/tests/components/flo/test_services.py +++ b/tests/components/flo/test_services.py @@ -17,7 +17,7 @@ from homeassistant.setup import async_setup_component from .common import TEST_PASSWORD, TEST_USER_ID -SWITCH_ENTITY_ID = "switch.shutoff_valve" +SWITCH_ENTITY_ID = "switch.smart_water_shutoff_shutoff_valve" async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mock): diff --git a/tests/components/flo/test_switch.py b/tests/components/flo/test_switch.py index cc6ab7e3a7e..5f97489fcde 100644 --- a/tests/components/flo/test_switch.py +++ b/tests/components/flo/test_switch.py @@ -17,7 +17,7 @@ async def test_valve_switches(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 - entity_id = "switch.shutoff_valve" + entity_id = "switch.smart_water_shutoff_shutoff_valve" assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( From edf304718cefc0f218c55f1170f038f82801ac1b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:41:12 -0600 Subject: [PATCH 304/820] Migrate Notion to new entity naming style (#74746) --- homeassistant/components/notion/__init__.py | 12 +++++++++--- homeassistant/components/notion/binary_sensor.py | 14 +++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 49277740f56..2a73d12d946 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -118,13 +118,18 @@ def _async_register_new_bridge( hass: HomeAssistant, bridge: dict, entry: ConfigEntry ) -> None: """Register a new bridge.""" + if name := bridge["name"]: + bridge_name = name.capitalize() + else: + bridge_name = bridge["id"] + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, bridge["hardware_id"])}, manufacturer="Silicon Labs", model=bridge["hardware_revision"], - name=bridge["name"] or bridge["id"], + name=bridge_name, sw_version=bridge["firmware_version"]["wifi"], ) @@ -132,6 +137,8 @@ def _async_register_new_bridge( class NotionEntity(CoordinatorEntity): """Define a base Notion entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, @@ -150,13 +157,12 @@ class NotionEntity(CoordinatorEntity): identifiers={(DOMAIN, sensor["hardware_id"])}, manufacturer="Silicon Labs", model=sensor["hardware_revision"], - name=str(sensor["name"]), + name=str(sensor["name"]).capitalize(), sw_version=sensor["firmware_version"], via_device=(DOMAIN, bridge.get("hardware_id")), ) self._attr_extra_state_attributes = {} - self._attr_name = f'{sensor["name"]}: {description.name}' self._attr_unique_id = ( f'{sensor_id}_{coordinator.data["tasks"][task_id]["task_type"]}' ) diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 57c70849a9a..2a34724837d 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -48,7 +48,7 @@ class NotionBinarySensorDescription( BINARY_SENSOR_DESCRIPTIONS = ( NotionBinarySensorDescription( key=SENSOR_BATTERY, - name="Low Battery", + name="Low battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state="critical", @@ -61,13 +61,13 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), NotionBinarySensorDescription( key=SENSOR_GARAGE_DOOR, - name="Garage Door", + name="Garage door", device_class=BinarySensorDeviceClass.GARAGE_DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_LEAK, - name="Leak Detector", + name="Leak detector", device_class=BinarySensorDeviceClass.MOISTURE, on_state="leak", ), @@ -86,25 +86,25 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), NotionBinarySensorDescription( key=SENSOR_SLIDING, - name="Sliding Door/Window", + name="Sliding door/window", device_class=BinarySensorDeviceClass.DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_SMOKE_CO, - name="Smoke/Carbon Monoxide Detector", + name="Smoke/Carbon monoxide detector", device_class=BinarySensorDeviceClass.SMOKE, on_state="alarm", ), NotionBinarySensorDescription( key=SENSOR_WINDOW_HINGED_HORIZONTAL, - name="Hinged Window", + name="Hinged window", device_class=BinarySensorDeviceClass.WINDOW, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_WINDOW_HINGED_VERTICAL, - name="Hinged Window", + name="Hinged window", device_class=BinarySensorDeviceClass.WINDOW, on_state="open", ), From 98a27ed3ed6e0b62aac8e5e0e1ebb3ad2139d028 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:48:58 -0600 Subject: [PATCH 305/820] Remove old RainMachine service descriptions (#74920) --- .../components/rainmachine/services.yaml | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index 7dcbe2cd8d7..198aec94a22 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -1,32 +1,4 @@ # Describes the format for available RainMachine services -disable_program: - name: Disable Program - description: Disable a program - target: - entity: - integration: rainmachine - domain: switch -disable_zone: - name: Disable Zone - description: Disable a zone - target: - entity: - integration: rainmachine - domain: switch -enable_program: - name: Enable Program - description: Enable a program - target: - entity: - integration: rainmachine - domain: switch -enable_zone: - name: Enable Zone - description: Enable a zone - target: - entity: - integration: rainmachine - domain: switch pause_watering: name: Pause All Watering description: Pause all watering activities for a number of seconds From 1aeb15050eefa56e6f7ddf74ae2186fd8f66b0d1 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Sun, 10 Jul 2022 21:52:49 +0200 Subject: [PATCH 306/820] Bump afsapi to 0.2.5 (#74907) --- homeassistant/components/frontier_silicon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 20092b941a9..12fb5145aa0 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.2.4"], + "requirements": ["afsapi==0.2.5"], "codeowners": ["@wlcrs"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2f1c35c8e83..9915f03e8f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,7 +89,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.2.4 +afsapi==0.2.5 # homeassistant.components.agent_dvr agent-py==0.0.23 From 23f2e9014e989ffa3c56ec4cf37b9647a39a925b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 21:59:38 +0200 Subject: [PATCH 307/820] Update feedparser to 6.0.10 (#74913) --- homeassistant/components/feedreader/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index 1a9bb05e140..9a50a905922 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -2,7 +2,7 @@ "domain": "feedreader", "name": "Feedreader", "documentation": "https://www.home-assistant.io/integrations/feedreader", - "requirements": ["feedparser==6.0.2"], + "requirements": ["feedparser==6.0.10"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["feedparser", "sgmllib3k"] diff --git a/requirements_all.txt b/requirements_all.txt index 9915f03e8f8..c136010056a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -638,7 +638,7 @@ faadelays==0.0.7 fastdotcom==0.0.3 # homeassistant.components.feedreader -feedparser==6.0.2 +feedparser==6.0.10 # homeassistant.components.fibaro fiblary3==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b24be8441f..dd29bcded25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -459,7 +459,7 @@ epson-projector==0.4.2 faadelays==0.0.7 # homeassistant.components.feedreader -feedparser==6.0.2 +feedparser==6.0.10 # homeassistant.components.fibaro fiblary3==0.1.8 From bb4b2014fc3bd8b1ce7f294ccf6fcc069ef00da5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:03:19 +0200 Subject: [PATCH 308/820] Migrate Wiz to new entity naming style (#74911) --- homeassistant/components/wiz/binary_sensor.py | 2 +- homeassistant/components/wiz/entity.py | 3 ++- homeassistant/components/wiz/number.py | 5 ++--- homeassistant/components/wiz/sensor.py | 5 ++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wiz/binary_sensor.py b/homeassistant/components/wiz/binary_sensor.py index 1ecb3125215..538bd3a741d 100644 --- a/homeassistant/components/wiz/binary_sensor.py +++ b/homeassistant/components/wiz/binary_sensor.py @@ -66,12 +66,12 @@ class WizOccupancyEntity(WizEntity, BinarySensorEntity): """Representation of WiZ Occupancy sensor.""" _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + _attr_name = "Occupancy" def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize an WiZ device.""" super().__init__(wiz_data, name) self._attr_unique_id = OCCUPANCY_UNIQUE_ID.format(self._device.mac) - self._attr_name = f"{name} Occupancy" self._async_update_attrs() @callback diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py index c78f3e3b37b..633fb71f165 100644 --- a/homeassistant/components/wiz/entity.py +++ b/homeassistant/components/wiz/entity.py @@ -21,13 +21,14 @@ from .models import WizData class WizEntity(CoordinatorEntity[DataUpdateCoordinator[Optional[float]]], Entity): """Representation of WiZ entity.""" + _attr_has_entity_name = True + def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize a WiZ entity.""" super().__init__(wiz_data.coordinator) self._device = wiz_data.bulb bulb_type: BulbType = self._device.bulbtype self._attr_unique_id = self._device.mac - self._attr_name = name self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, self._device.mac)}, name=name, diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index d2f68fcf7c3..9fd700f8f9c 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -52,7 +52,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_max_value=200, native_step=1, icon="mdi:speedometer", - name="Effect Speed", + name="Effect speed", value_fn=lambda device: cast(Optional[int], device.state.get_speed()), set_value_fn=_async_set_speed, required_feature="effect", @@ -63,7 +63,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_max_value=100, native_step=1, icon="mdi:floor-lamp-dual", - name="Dual Head Ratio", + name="Dual head ratio", value_fn=lambda device: cast(Optional[int], device.state.get_ratio()), set_value_fn=_async_set_ratio, required_feature="dual_head", @@ -98,7 +98,6 @@ class WizSpeedNumber(WizEntity, NumberEntity): super().__init__(wiz_data, name) self.entity_description = description self._attr_unique_id = f"{self._device.mac}_{description.key}" - self._attr_name = f"{name} {description.name}" self._async_update_attrs() @property diff --git a/homeassistant/components/wiz/sensor.py b/homeassistant/components/wiz/sensor.py index 11f3933fd16..d2042d6ea9c 100644 --- a/homeassistant/components/wiz/sensor.py +++ b/homeassistant/components/wiz/sensor.py @@ -20,7 +20,7 @@ from .models import WizData SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="rssi", - name="Signal Strength", + name="Signal strength", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -33,7 +33,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( POWER_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="power", - name="Current Power", + name="Current power", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, @@ -73,7 +73,6 @@ class WizSensor(WizEntity, SensorEntity): super().__init__(wiz_data, name) self.entity_description = description self._attr_unique_id = f"{self._device.mac}_{description.key}" - self._attr_name = f"{name} {description.name}" self._async_update_attrs() @callback From c5253d3da039713c204a702228e44c8dff7bc449 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:05:00 +0200 Subject: [PATCH 309/820] Migrate Geocaching to new entity naming style (#74899) --- homeassistant/components/geocaching/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/geocaching/sensor.py b/homeassistant/components/geocaching/sensor.py index 0c719c463a4..134877d7509 100644 --- a/homeassistant/components/geocaching/sensor.py +++ b/homeassistant/components/geocaching/sensor.py @@ -91,6 +91,7 @@ class GeocachingSensor( """Representation of a Sensor.""" entity_description: GeocachingSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -100,9 +101,6 @@ class GeocachingSensor( """Initialize the Geocaching sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = ( - f"Geocaching {coordinator.data.user.username} {description.name}" - ) self._attr_unique_id = ( f"{coordinator.data.user.reference_code}_{description.key}" ) From b070bb8ef097cc221ae16c85722e6e0f8cf3e4b0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:05:54 +0200 Subject: [PATCH 310/820] Migrate Supervisor integration to new entity naming style (#74906) --- homeassistant/components/hassio/binary_sensor.py | 2 +- homeassistant/components/hassio/entity.py | 13 ++++++++----- homeassistant/components/hassio/sensor.py | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index 85cb402b0ca..6ddc15e7725 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -36,7 +36,7 @@ COMMON_ENTITY_DESCRIPTIONS = ( device_class=BinarySensorDeviceClass.UPDATE, entity_registry_enabled_default=False, key=ATTR_UPDATE_AVAILABLE, - name="Update Available", + name="Update available", ), ) diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index c6bec04123c..dfa89ae911a 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any -from homeassistant.const import ATTR_NAME from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -20,6 +19,8 @@ from .const import ( class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base entity for a Hass.io add-on.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -30,7 +31,6 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): super().__init__(coordinator) self.entity_description = entity_description self._addon_slug = addon[ATTR_SLUG] - self._attr_name = f"{addon[ATTR_NAME]}: {entity_description.name}" self._attr_unique_id = f"{addon[ATTR_SLUG]}_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, addon[ATTR_SLUG])}) @@ -48,6 +48,8 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Hass.io OS.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -56,7 +58,6 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Initialize base entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = f"Home Assistant Operating System: {entity_description.name}" self._attr_unique_id = f"home_assistant_os_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "OS")}) @@ -73,6 +74,8 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Supervisor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -81,7 +84,6 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Initialize base entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = f"Home Assistant Supervisor: {entity_description.name}" self._attr_unique_id = f"home_assistant_supervisor_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "supervisor")}) @@ -99,6 +101,8 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Core.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -107,7 +111,6 @@ class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Initialize base entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = f"Home Assistant Core: {entity_description.name}" self._attr_unique_id = f"home_assistant_core_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "core")}) diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 55fcb0bcd28..31e728a9736 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -31,7 +31,7 @@ COMMON_ENTITY_DESCRIPTIONS = ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_VERSION_LATEST, - name="Newest Version", + name="Newest version", ), ) @@ -39,7 +39,7 @@ ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_CPU_PERCENT, - name="CPU Percent", + name="CPU percent", icon="mdi:cpu-64-bit", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -47,7 +47,7 @@ ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_MEMORY_PERCENT, - name="Memory Percent", + name="Memory percent", icon="mdi:memory", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, From 4433065438b6d6dff1db0da771b70f9026a49cf0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:09:42 -0600 Subject: [PATCH 311/820] Migrate Ridwell to new entity naming style (#74915) --- homeassistant/components/ridwell/__init__.py | 2 ++ homeassistant/components/ridwell/sensor.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 6b615719bd2..5f3656a8b5a 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -124,6 +124,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class RidwellEntity(CoordinatorEntity): """Define a base Ridwell entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 44bab16691a..f44cac134d8 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -28,7 +28,7 @@ ATTR_QUANTITY = "quantity" SENSOR_DESCRIPTION = SensorEntityDescription( key=SENSOR_TYPE_NEXT_PICKUP, - name="Ridwell Pickup", + name="Ridwell pickup", device_class=SensorDeviceClass.DATE, ) From 07444dba2a053be8c191dcc522bcaf3321acd5d2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:11:02 -0600 Subject: [PATCH 312/820] Migrate ReCollect Waste to new entity naming style (#74914) --- homeassistant/components/recollect_waste/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index fab482a00e6..7d527ac56c6 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -28,11 +28,11 @@ SENSOR_TYPE_NEXT_PICKUP = "next_pickup" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_CURRENT_PICKUP, - name="Current Pickup", + name="Current pickup", ), SensorEntityDescription( key=SENSOR_TYPE_NEXT_PICKUP, - name="Next Pickup", + name="Next pickup", ), ) @@ -68,6 +68,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """ReCollect Waste Sensor.""" _attr_device_class = SensorDeviceClass.DATE + _attr_has_entity_name = True def __init__( self, From 4a38be2924b107e55653046560940205982a5ede Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:12:56 -0600 Subject: [PATCH 313/820] Migrate WattTime to new entity naming style (#74916) --- homeassistant/components/watttime/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 41842c6ed70..040753d97e8 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -35,14 +35,14 @@ SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT = "percent" REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_MOER, - name="Marginal Operating Emissions Rate", + name="Marginal operating emissions rate", icon="mdi:blur", native_unit_of_measurement=f"{MASS_POUNDS} CO2/MWh", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT, - name="Relative Marginal Emissions Intensity", + name="Relative marginal emissions intensity", icon="mdi:blur", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -67,6 +67,8 @@ async def async_setup_entry( class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): """Define a realtime emissions sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, From c81d63e070029b1a86799778f2f8789c78fcc0a8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:13:09 +0200 Subject: [PATCH 314/820] Migrate Cast to new entity naming style (#74901) --- homeassistant/components/cast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index c8a6a82571e..da32dfd6ae7 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -274,6 +274,7 @@ class CastDevice: class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Representation of a Cast device on the network.""" + _attr_has_entity_name = True _attr_should_poll = False _attr_media_image_remotely_accessible = True _mz_only = False @@ -293,7 +294,6 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): self._cast_view_remove_handler = None self._attr_unique_id = str(cast_info.uuid) - self._attr_name = cast_info.friendly_name self._attr_device_info = DeviceInfo( identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, manufacturer=str(cast_info.cast_info.manufacturer), From d6ceebbb68a15de8a55674f18b8e92827e33e6d2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:13:22 +0200 Subject: [PATCH 315/820] Migrate Met.no to new entity naming style (#74908) --- homeassistant/components/met/weather.py | 3 ++- tests/components/met/test_weather.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 0ff0a60bfa1..c843be73fe7 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -71,6 +71,7 @@ def format_condition(condition: str) -> str: class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" + _attr_has_entity_name = True _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_precipitation_unit = LENGTH_MILLIMETERS _attr_native_pressure_unit = PRESSURE_HPA @@ -111,7 +112,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): name = self._config.get(CONF_NAME) name_appendix = "" if self._hourly: - name_appendix = " Hourly" + name_appendix = " hourly" if name is not None: return f"{name}{name_appendix}" diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 3025356d8fb..c9fde5c8ff7 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -16,10 +16,10 @@ async def test_tracking_home(hass, mock_weather): # Test the hourly sensor is disabled by default registry = er.async_get(hass) - state = hass.states.get("weather.test_home_hourly") + state = hass.states.get("weather.forecast_test_home_hourly") assert state is None - entry = registry.async_get("weather.test_home_hourly") + entry = registry.async_get("weather.forecast_test_home_hourly") assert entry assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION @@ -50,7 +50,7 @@ async def test_not_tracking_home(hass, mock_weather): WEATHER_DOMAIN, DOMAIN, "10-20-hourly", - suggested_object_id="somewhere_hourly", + suggested_object_id="forecast_somewhere_hourly", disabled_by=None, ) From 8285f42d267cdd799be00a0cafed6bea5454f932 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:16:16 -0600 Subject: [PATCH 316/820] Migrate IQVIA to new entity naming style (#74917) --- homeassistant/components/iqvia/__init__.py | 2 ++ homeassistant/components/iqvia/sensor.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 686f5b57f05..aad505e23c4 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -118,6 +118,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class IQVIAEntity(CoordinatorEntity): """Define a base IQVIA entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index d8c7ea317c8..033ed4e3031 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -79,17 +79,17 @@ TREND_SUBSIDING = "Subsiding" FORECAST_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_ALLERGY_FORECAST, - name="Allergy Index: Forecasted Average", + name="Allergy index: forecasted average", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_ASTHMA_FORECAST, - name="Asthma Index: Forecasted Average", + name="Asthma index: forecasted average", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_DISEASE_FORECAST, - name="Cold & Flu: Forecasted Average", + name="Cold & flu: forecasted average", icon="mdi:snowflake", ), ) @@ -97,29 +97,29 @@ FORECAST_SENSOR_DESCRIPTIONS = ( INDEX_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_ALLERGY_TODAY, - name="Allergy Index: Today", + name="Allergy index: today", icon="mdi:flower", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_ALLERGY_TOMORROW, - name="Allergy Index: Tomorrow", + name="Allergy index: tomorrow", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_ASTHMA_TODAY, - name="Asthma Index: Today", + name="Asthma index: today", icon="mdi:flower", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_ASTHMA_TOMORROW, - name="Asthma Index: Tomorrow", + name="Asthma index: tomorrow", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_DISEASE_TODAY, - name="Cold & Flu Index: Today", + name="Cold & flu index: today", icon="mdi:pill", state_class=SensorStateClass.MEASUREMENT, ), From 6f28e4bfee341013e802947c955d66e429a34d7f Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 10 Jul 2022 16:17:59 -0400 Subject: [PATCH 317/820] Migrate ZHA to new entity naming standard (#74846) --- homeassistant/components/zha/entity.py | 17 +- tests/components/zha/common.py | 26 +- tests/components/zha/test_cover.py | 2 +- tests/components/zha/test_device.py | 2 +- tests/components/zha/test_device_action.py | 8 +- tests/components/zha/test_discover.py | 4 +- tests/components/zha/test_number.py | 4 +- tests/components/zha/test_select.py | 8 +- tests/components/zha/test_sensor.py | 34 +- tests/components/zha/zha_devices_list.py | 2312 ++++++++++---------- 10 files changed, 1210 insertions(+), 1207 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index f70948eb04a..8b3627df9de 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -45,6 +45,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): """A base class for ZHA entities.""" unique_id_suffix: str | None = None + _attr_has_entity_name = True def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None: """Init ZHA entity.""" @@ -173,11 +174,13 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): ) -> None: """Init ZHA entity.""" super().__init__(unique_id, zha_device, **kwargs) - ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) - ch_names = ", ".join(sorted(ch.name for ch in channels)) - self._name: str = f"{zha_device.name} {ieeetail} {ch_names}" - if self.unique_id_suffix: - self._name += f" {self.unique_id_suffix}" + self._name: str = ( + self.__class__.__name__.lower() + .replace("zha", "") + .replace("entity", "") + .replace("sensor", "") + .capitalize() + ) self.cluster_channels: dict[str, ZigbeeChannel] = {} for channel in channels: self.cluster_channels[channel.name] = channel @@ -260,7 +263,9 @@ class ZhaGroupEntity(BaseZhaEntity): super().__init__(unique_id, zha_device, **kwargs) self._available = False self._group = zha_device.gateway.groups.get(group_id) - self._name = f"{self._group.name}_zha_group_0x{group_id:04x}" + self._name = ( + f"{self._group.name}_zha_group_0x{group_id:04x}".lower().capitalize() + ) self._group_id: int = group_id self._entity_ids: list[str] = entity_ids self._async_unsub_state_changed: CALLBACK_TYPE | None = None diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 757587071fd..6a51f441a78 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -7,7 +7,7 @@ import zigpy.zcl import zigpy.zcl.foundation as zcl_f import homeassistant.components.zha.core.const as zha_const -from homeassistant.util import slugify +from homeassistant.helpers import entity_registry def patch_cluster(cluster): @@ -133,7 +133,7 @@ async def find_entity_id(domain, zha_device, hass, qualifier=None): This is used to get the entity id in order to get the state from the state machine so that we can test state changes. """ - entities = await find_entity_ids(domain, zha_device, hass) + entities = find_entity_ids(domain, zha_device, hass) if not entities: return None if qualifier: @@ -144,28 +144,26 @@ async def find_entity_id(domain, zha_device, hass, qualifier=None): return entities[0] -async def find_entity_ids(domain, zha_device, hass): +def find_entity_ids(domain, zha_device, hass): """Find the entity ids under the testing. This is used to get the entity id in order to get the state from the state machine so that we can test state changes. """ - ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) - head = f"{domain}.{slugify(f'{zha_device.name} {ieeetail}')}" - enitiy_ids = hass.states.async_entity_ids(domain) - await hass.async_block_till_done() - - res = [] - for entity_id in enitiy_ids: - if entity_id.startswith(head): - res.append(entity_id) - return res + registry = entity_registry.async_get(hass) + return [ + entity.entity_id + for entity in entity_registry.async_entries_for_device( + registry, zha_device.device_id + ) + if entity.domain == domain + ] def async_find_group_entity_id(hass, domain, group): """Find the group entity id under test.""" - entity_id = f"{domain}.{group.name.lower().replace(' ','_')}_zha_group_0x{group.group_id:04x}" + entity_id = f"{domain}.fakemanufacturer_fakemodel_{group.name.lower().replace(' ','_')}_zha_group_0x{group.group_id:04x}" entity_ids = hass.states.async_entity_ids(domain) diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 3dab405151d..0f55735ecb2 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -345,7 +345,7 @@ async def test_restore_state(hass, zha_device_restored, zigpy_shade_device): hass, ( State( - "cover.fakemanufacturer_fakemodel_e769900a_level_on_off_shade", + "cover.fakemanufacturer_fakemodel_shade", STATE_OPEN, {ATTR_CURRENT_POSITION: 50}, ), diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 8b718635b6a..f05a5cd1872 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -311,7 +311,7 @@ async def test_device_restore_availability( zha_device = await zha_device_restored( zigpy_device, last_seen=time.time() - last_seen_delta ) - entity_id = "switch.fakemanufacturer_fakemodel_e769900a_on_off" + entity_id = "switch.fakemanufacturer_fakemodel_switch" await hass.async_block_till_done() # ensure the switch entity was created diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index fffb79fe0f2..f24ec054c0b 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -86,28 +86,28 @@ async def test_get_actions(hass, device_ias): "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_warningmode", + "entity_id": "select.fakemanufacturer_fakemodel_defaulttoneselect", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_sirenlevel", + "entity_id": "select.fakemanufacturer_fakemodel_defaultsirenlevelselect", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_strobelevel", + "entity_id": "select.fakemanufacturer_fakemodel_defaultstrobelevelselect", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_strobe", + "entity_id": "select.fakemanufacturer_fakemodel_defaultstrobeselect", "metadata": {"secondary": True}, }, ] diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 4de251fda8b..0f51141ec5d 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -439,8 +439,8 @@ def test_single_input_cluster_device_class_by_cluster_class(): @pytest.mark.parametrize( "override, entity_id", [ - (None, "light.manufacturer_model_77665544_level_light_color_on_off"), - ("switch", "switch.manufacturer_model_77665544_on_off"), + (None, "light.manufacturer_model_light"), + ("switch", "switch.manufacturer_model_switch"), ], ) async def test_device_override( diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index f6b606ccbbf..808649b7f55 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -133,7 +133,7 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi assert hass.states.get(entity_id).attributes.get("unit_of_measurement") == "%" assert ( hass.states.get(entity_id).attributes.get("friendly_name") - == "FakeManufacturer FakeModel e769900a analog_output PWM1" + == "FakeManufacturer FakeModel Number PWM1" ) # change value from device @@ -210,7 +210,7 @@ async def test_level_control_number( Platform.NUMBER, zha_device, hass, - qualifier=attr, + qualifier=attr.replace("_", ""), ) assert entity_id is not None diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index c883d648e8e..1c714def54b 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -113,12 +113,11 @@ async def test_select(hass, siren): entity_registry = er.async_get(hass) zha_device, cluster = siren assert cluster is not None - select_name = security.IasWd.Warning.WarningMode.__name__ entity_id = await find_entity_id( Platform.SELECT, zha_device, hass, - qualifier=select_name.lower(), + qualifier="tone", ) assert entity_id is not None @@ -163,7 +162,7 @@ async def test_select_restore_state( ): """Test zha select entity restore state.""" - entity_id = "select.fakemanufacturer_fakemodel_e769900a_ias_wd_warningmode" + entity_id = "select.fakemanufacturer_fakemodel_defaulttoneselect" core_rs(entity_id, state="Burglar") zigpy_device = zigpy_device_mock( @@ -180,12 +179,11 @@ async def test_select_restore_state( zha_device = await zha_device_restored(zigpy_device) cluster = zigpy_device.endpoints[1].ias_wd assert cluster is not None - select_name = security.IasWd.Warning.WarningMode.__name__ entity_id = await find_entity_id( Platform.SELECT, zha_device, hass, - qualifier=select_name.lower(), + qualifier="tone", ) assert entity_id is not None diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index c638bdd8c48..d2fc7c3ca73 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -48,7 +48,7 @@ from .common import ( ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_e769900a_{}" +ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_{}" @pytest.fixture(autouse=True) @@ -312,7 +312,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( smartenergy.Metering.cluster_id, - "smartenergy_metering_summation_delivered", + "smartenergy_summation", async_test_smart_energy_summation, 1, { @@ -360,7 +360,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( general.PowerConfiguration.cluster_id, - "power", + "battery", async_test_powerconfiguration, 2, { @@ -414,7 +414,7 @@ async def test_sensor( zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 cluster.PLUGGED_ATTR_READS = read_plug zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = ENTITY_ID_PREFIX.format(entity_suffix) + entity_id = ENTITY_ID_PREFIX.format(entity_suffix.replace("_", "")) await async_enable_traffic(hass, [zha_device], enabled=False) await hass.async_block_till_done() @@ -620,7 +620,7 @@ async def test_electrical_measurement_init( {"apparent_power", "rms_voltage", "rms_current"}, { "electrical_measurement", - "electrical_measurement_ac_frequency", + "electrical_measurement_frequency", "electrical_measurement_power_factor", }, { @@ -636,7 +636,7 @@ async def test_electrical_measurement_init( { "electrical_measurement_apparent_power", "electrical_measurement_rms_current", - "electrical_measurement_ac_frequency", + "electrical_measurement_frequency", "electrical_measurement_power_factor", }, ), @@ -648,7 +648,7 @@ async def test_electrical_measurement_init( "electrical_measurement", "electrical_measurement_apparent_power", "electrical_measurement_rms_current", - "electrical_measurement_ac_frequency", + "electrical_measurement_frequency", "electrical_measurement_power_factor", }, set(), @@ -659,7 +659,7 @@ async def test_electrical_measurement_init( "instantaneous_demand", }, { - "smartenergy_metering_summation_delivered", + "smartenergy_summation", }, { "smartenergy_metering", @@ -670,7 +670,7 @@ async def test_electrical_measurement_init( {"instantaneous_demand", "current_summ_delivered"}, {}, { - "smartenergy_metering_summation_delivered", + "smartenergy_summation", "smartenergy_metering", }, ), @@ -678,7 +678,7 @@ async def test_electrical_measurement_init( smartenergy.Metering.cluster_id, {}, { - "smartenergy_metering_summation_delivered", + "smartenergy_summation", "smartenergy_metering", }, {}, @@ -696,8 +696,10 @@ async def test_unsupported_attributes_sensor( ): """Test zha sensor platform.""" - entity_ids = {ENTITY_ID_PREFIX.format(e) for e in entity_ids} - missing_entity_ids = {ENTITY_ID_PREFIX.format(e) for e in missing_entity_ids} + entity_ids = {ENTITY_ID_PREFIX.format(e.replace("_", "")) for e in entity_ids} + missing_entity_ids = { + ENTITY_ID_PREFIX.format(e.replace("_", "")) for e in missing_entity_ids + } zigpy_device = zigpy_device_mock( { @@ -718,7 +720,7 @@ async def test_unsupported_attributes_sensor( await async_enable_traffic(hass, [zha_device], enabled=False) await hass.async_block_till_done() - present_entity_ids = set(await find_entity_ids(Platform.SENSOR, zha_device, hass)) + present_entity_ids = set(find_entity_ids(Platform.SENSOR, zha_device, hass)) assert present_entity_ids == entity_ids assert missing_entity_ids not in present_entity_ids @@ -811,7 +813,7 @@ async def test_se_summation_uom( ): """Test zha smart energy summation.""" - entity_id = ENTITY_ID_PREFIX.format("smartenergy_metering_summation_delivered") + entity_id = ENTITY_ID_PREFIX.format("smartenergysummation") zigpy_device = zigpy_device_mock( { 1: { @@ -865,7 +867,7 @@ async def test_elec_measurement_sensor_type( ): """Test zha electrical measurement sensor type.""" - entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + entity_id = ENTITY_ID_PREFIX.format("electricalmeasurement") zigpy_dev = elec_measurement_zigpy_dev zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[ "measurement_type" @@ -914,7 +916,7 @@ async def test_elec_measurement_skip_unsupported_attribute( ): """Test zha electrical measurement skipping update of unsupported attributes.""" - entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + entity_id = ENTITY_ID_PREFIX.format("electricalmeasurement") zha_dev = elec_measurement_zha_dev cluster = zha_dev.device.endpoints[1].electrical_measurement diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 9a32a1670b6..4cea8eb8f66 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -38,25 +38,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008"], DEV_SIG_ENTITIES: [ - "button.adurolight_adurolight_ncc_77665544_identify", - "sensor.adurolight_adurolight_ncc_77665544_basic_rssi", - "sensor.adurolight_adurolight_ncc_77665544_basic_lqi", + "button.adurolight_adurolight_ncc_identifybutton", + "sensor.adurolight_adurolight_ncc_rssi", + "sensor.adurolight_adurolight_ncc_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_lqi", }, }, }, @@ -76,43 +76,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["5:0x0019"], DEV_SIG_ENTITIES: [ - "button.bosch_isw_zpr1_wp13_77665544_identify", - "sensor.bosch_isw_zpr1_wp13_77665544_power", - "sensor.bosch_isw_zpr1_wp13_77665544_temperature", - "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", - "sensor.bosch_isw_zpr1_wp13_77665544_basic_rssi", - "sensor.bosch_isw_zpr1_wp13_77665544_basic_lqi", + "button.bosch_isw_zpr1_wp13_identifybutton", + "sensor.bosch_isw_zpr1_wp13_battery", + "sensor.bosch_isw_zpr1_wp13_temperature", + "binary_sensor.bosch_isw_zpr1_wp13_iaszone", + "sensor.bosch_isw_zpr1_wp13_rssi", + "sensor.bosch_isw_zpr1_wp13_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-5-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.bosch_isw_zpr1_wp13_iaszone", }, ("button", "00:11:22:33:44:55:66:77-5-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-5-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_battery", }, ("sensor", "00:11:22:33:44:55:66:77-5-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-5-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-5-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_lqi", }, }, }, @@ -132,31 +132,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3130_77665544_identify", - "sensor.centralite_3130_77665544_power", - "sensor.centralite_3130_77665544_basic_rssi", - "sensor.centralite_3130_77665544_basic_lqi", + "button.centralite_3130_identifybutton", + "sensor.centralite_3130_battery", + "sensor.centralite_3130_rssi", + "sensor.centralite_3130_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3130_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3130_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_lqi", }, }, }, @@ -176,79 +176,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3210_l_77665544_identify", - "sensor.centralite_3210_l_77665544_electrical_measurement", - "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", - "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", - "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", - "sensor.centralite_3210_l_77665544_electrical_measurement_ac_frequency", - "sensor.centralite_3210_l_77665544_electrical_measurement_power_factor", - "sensor.centralite_3210_l_77665544_smartenergy_metering", - "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", - "switch.centralite_3210_l_77665544_on_off", - "sensor.centralite_3210_l_77665544_basic_rssi", - "sensor.centralite_3210_l_77665544_basic_lqi", + "button.centralite_3210_l_identifybutton", + "sensor.centralite_3210_l_electricalmeasurement", + "sensor.centralite_3210_l_electricalmeasurementapparentpower", + "sensor.centralite_3210_l_electricalmeasurementrmscurrent", + "sensor.centralite_3210_l_electricalmeasurementrmsvoltage", + "sensor.centralite_3210_l_electricalmeasurementfrequency", + "sensor.centralite_3210_l_electricalmeasurementpowerfactor", + "sensor.centralite_3210_l_smartenergymetering", + "sensor.centralite_3210_l_smartenergysummation", + "switch.centralite_3210_l_switch", + "sensor.centralite_3210_l_rssi", + "sensor.centralite_3210_l_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.centralite_3210_l_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.centralite_3210_l_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_lqi", }, }, }, @@ -268,43 +268,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3310_s_77665544_identify", - "sensor.centralite_3310_s_77665544_power", - "sensor.centralite_3310_s_77665544_temperature", - "sensor.centralite_3310_s_77665544_manufacturer_specific", - "sensor.centralite_3310_s_77665544_basic_rssi", - "sensor.centralite_3310_s_77665544_basic_lqi", + "button.centralite_3310_s_identifybutton", + "sensor.centralite_3310_s_battery", + "sensor.centralite_3310_s_temperature", + "sensor.centralite_3310_s_humidity", + "sensor.centralite_3310_s_rssi", + "sensor.centralite_3310_s_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-64581"): { DEV_SIG_CHANNELS: ["manufacturer_specific"], DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_manufacturer_specific", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_humidity", }, }, }, @@ -331,43 +331,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3315_s_77665544_identify", - "sensor.centralite_3315_s_77665544_power", - "sensor.centralite_3315_s_77665544_temperature", - "binary_sensor.centralite_3315_s_77665544_ias_zone", - "sensor.centralite_3315_s_77665544_basic_rssi", - "sensor.centralite_3315_s_77665544_basic_lqi", + "button.centralite_3315_s_identifybutton", + "sensor.centralite_3315_s_battery", + "sensor.centralite_3315_s_temperature", + "binary_sensor.centralite_3315_s_iaszone", + "sensor.centralite_3315_s_rssi", + "sensor.centralite_3315_s_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3315_s_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3315_s_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_lqi", }, }, }, @@ -394,43 +394,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3320_l_77665544_identify", - "sensor.centralite_3320_l_77665544_power", - "sensor.centralite_3320_l_77665544_temperature", - "binary_sensor.centralite_3320_l_77665544_ias_zone", - "sensor.centralite_3320_l_77665544_basic_rssi", - "sensor.centralite_3320_l_77665544_basic_lqi", + "button.centralite_3320_l_identifybutton", + "sensor.centralite_3320_l_battery", + "sensor.centralite_3320_l_temperature", + "binary_sensor.centralite_3320_l_iaszone", + "sensor.centralite_3320_l_rssi", + "sensor.centralite_3320_l_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3320_l_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3320_l_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_lqi", }, }, }, @@ -457,43 +457,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3326_l_77665544_identify", - "sensor.centralite_3326_l_77665544_power", - "sensor.centralite_3326_l_77665544_temperature", - "binary_sensor.centralite_3326_l_77665544_ias_zone", - "sensor.centralite_3326_l_77665544_basic_rssi", - "sensor.centralite_3326_l_77665544_basic_lqi", + "button.centralite_3326_l_identifybutton", + "sensor.centralite_3326_l_battery", + "sensor.centralite_3326_l_temperature", + "binary_sensor.centralite_3326_l_iaszone", + "sensor.centralite_3326_l_rssi", + "sensor.centralite_3326_l_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3326_l_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3326_l_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_lqi", }, }, }, @@ -520,49 +520,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_motion_sensor_a_77665544_identify", - "sensor.centralite_motion_sensor_a_77665544_power", - "sensor.centralite_motion_sensor_a_77665544_temperature", - "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", - "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", - "sensor.centralite_motion_sensor_a_77665544_basic_rssi", - "sensor.centralite_motion_sensor_a_77665544_basic_lqi", + "button.centralite_motion_sensor_a_identifybutton", + "sensor.centralite_motion_sensor_a_battery", + "sensor.centralite_motion_sensor_a_temperature", + "binary_sensor.centralite_motion_sensor_a_iaszone", + "binary_sensor.centralite_motion_sensor_a_occupancy", + "sensor.centralite_motion_sensor_a_rssi", + "sensor.centralite_motion_sensor_a_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-1030"): { DEV_SIG_CHANNELS: ["occupancy"], DEV_SIG_ENT_MAP_CLASS: "Occupancy", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_occupancy", }, }, }, @@ -589,43 +589,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["4:0x0019"], DEV_SIG_ENTITIES: [ - "button.climaxtechnology_psmp5_00_00_02_02tc_77665544_identify", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", - "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_rssi", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_lqi", + "button.climaxtechnology_psmp5_00_00_02_02tc_identifybutton", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergymetering", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergysummation", + "switch.climaxtechnology_psmp5_00_00_02_02tc_switch", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_rssi", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.climaxtechnology_psmp5_00_00_02_02tc_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_psmp5_00_00_02_02tc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_psmp5_00_00_02_02tc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_lqi", }, }, }, @@ -645,61 +645,61 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", - "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", - "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_rssi", - "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_warningmode", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_sirenlevel", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobelevel", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobe", - "siren.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd", + "button.climaxtechnology_sd8sc_00_00_03_12tc_identifybutton", + "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_iaszone", + "sensor.climaxtechnology_sd8sc_00_00_03_12tc_rssi", + "sensor.climaxtechnology_sd8sc_00_00_03_12tc_lqi", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaulttoneselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultsirenlevelselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobelevelselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobeselect", + "siren.climaxtechnology_sd8sc_00_00_03_12tc_siren", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_lqi", }, ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_warningmode", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaulttoneselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_sirenlevel", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultsirenlevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobelevel", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobelevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobe", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobeselect", }, ("siren", "00:11:22:33:44:55:66:77-1-1282"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd", + DEV_SIG_ENT_MAP_ID: "siren.climaxtechnology_sd8sc_00_00_03_12tc_siren", }, }, }, @@ -719,31 +719,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", - "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", - "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_rssi", - "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_lqi", + "button.climaxtechnology_ws15_00_00_03_03tc_identifybutton", + "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_iaszone", + "sensor.climaxtechnology_ws15_00_00_03_03tc_rssi", + "sensor.climaxtechnology_ws15_00_00_03_03tc_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_lqi", }, }, }, @@ -770,31 +770,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", - "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", - "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_rssi", - "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_lqi", + "button.feibit_inc_co_fb56_zcw08ku1_1_identifybutton", + "light.feibit_inc_co_fb56_zcw08ku1_1_light", + "sensor.feibit_inc_co_fb56_zcw08ku1_1_rssi", + "sensor.feibit_inc_co_fb56_zcw08ku1_1_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-11"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.feibit_inc_co_fb56_zcw08ku1_1_light", }, ("button", "00:11:22:33:44:55:66:77-11-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-11-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-11-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_lqi", }, }, }, @@ -814,67 +814,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.heiman_smokesensor_em_77665544_identify", - "sensor.heiman_smokesensor_em_77665544_power", - "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", - "sensor.heiman_smokesensor_em_77665544_basic_rssi", - "sensor.heiman_smokesensor_em_77665544_basic_lqi", - "select.heiman_smokesensor_em_77665544_ias_wd_warningmode", - "select.heiman_smokesensor_em_77665544_ias_wd_sirenlevel", - "select.heiman_smokesensor_em_77665544_ias_wd_strobelevel", - "select.heiman_smokesensor_em_77665544_ias_wd_strobe", - "siren.heiman_smokesensor_em_77665544_ias_wd", + "button.heiman_smokesensor_em_identifybutton", + "sensor.heiman_smokesensor_em_battery", + "binary_sensor.heiman_smokesensor_em_iaszone", + "sensor.heiman_smokesensor_em_rssi", + "sensor.heiman_smokesensor_em_lqi", + "select.heiman_smokesensor_em_defaulttoneselect", + "select.heiman_smokesensor_em_defaultsirenlevelselect", + "select.heiman_smokesensor_em_defaultstrobelevelselect", + "select.heiman_smokesensor_em_defaultstrobeselect", + "siren.heiman_smokesensor_em_siren", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_smokesensor_em_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_lqi", }, ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_warningmode", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaulttoneselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_sirenlevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultsirenlevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_strobelevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultstrobelevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_strobe", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultstrobeselect", }, ("siren", "00:11:22:33:44:55:66:77-1-1282"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.heiman_smokesensor_em_77665544_ias_wd", + DEV_SIG_ENT_MAP_ID: "siren.heiman_smokesensor_em_siren", }, }, }, @@ -894,31 +894,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.heiman_co_v16_77665544_identify", - "binary_sensor.heiman_co_v16_77665544_ias_zone", - "sensor.heiman_co_v16_77665544_basic_rssi", - "sensor.heiman_co_v16_77665544_basic_lqi", + "button.heiman_co_v16_identifybutton", + "binary_sensor.heiman_co_v16_iaszone", + "sensor.heiman_co_v16_rssi", + "sensor.heiman_co_v16_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_co_v16_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_co_v16_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_lqi", }, }, }, @@ -938,61 +938,61 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.heiman_warningdevice_77665544_identify", - "binary_sensor.heiman_warningdevice_77665544_ias_zone", - "sensor.heiman_warningdevice_77665544_basic_rssi", - "sensor.heiman_warningdevice_77665544_basic_lqi", - "select.heiman_warningdevice_77665544_ias_wd_warningmode", - "select.heiman_warningdevice_77665544_ias_wd_sirenlevel", - "select.heiman_warningdevice_77665544_ias_wd_strobelevel", - "select.heiman_warningdevice_77665544_ias_wd_strobe", - "siren.heiman_warningdevice_77665544_ias_wd", + "button.heiman_warningdevice_identifybutton", + "binary_sensor.heiman_warningdevice_iaszone", + "sensor.heiman_warningdevice_rssi", + "sensor.heiman_warningdevice_lqi", + "select.heiman_warningdevice_defaulttoneselect", + "select.heiman_warningdevice_defaultsirenlevelselect", + "select.heiman_warningdevice_defaultstrobelevelselect", + "select.heiman_warningdevice_defaultstrobeselect", + "siren.heiman_warningdevice_siren", ], DEV_SIG_ENT_MAP: { ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_warningmode", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaulttoneselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_sirenlevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultsirenlevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_strobelevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultstrobelevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_strobe", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultstrobeselect", }, ("siren", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.heiman_warningdevice_77665544_ias_wd", + DEV_SIG_ENT_MAP_ID: "siren.heiman_warningdevice_siren", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_lqi", }, }, }, @@ -1012,49 +1012,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["6:0x0019"], DEV_SIG_ENTITIES: [ - "button.hivehome_com_mot003_77665544_identify", - "sensor.hivehome_com_mot003_77665544_power", - "sensor.hivehome_com_mot003_77665544_illuminance", - "sensor.hivehome_com_mot003_77665544_temperature", - "binary_sensor.hivehome_com_mot003_77665544_ias_zone", - "sensor.hivehome_com_mot003_77665544_basic_rssi", - "sensor.hivehome_com_mot003_77665544_basic_lqi", + "button.hivehome_com_mot003_identifybutton", + "sensor.hivehome_com_mot003_battery", + "sensor.hivehome_com_mot003_illuminance", + "sensor.hivehome_com_mot003_temperature", + "binary_sensor.hivehome_com_mot003_iaszone", + "sensor.hivehome_com_mot003_rssi", + "sensor.hivehome_com_mot003_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-6-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.hivehome_com_mot003_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.hivehome_com_mot003_iaszone", }, ("button", "00:11:22:33:44:55:66:77-6-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-6-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_battery", }, ("sensor", "00:11:22:33:44:55:66:77-6-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-6-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-6-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-6-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_lqi", }, }, }, @@ -1081,31 +1081,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_lqi", }, }, }, @@ -1125,31 +1125,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_lqi", }, }, }, @@ -1169,31 +1169,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_lqi", }, }, }, @@ -1213,31 +1213,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_lqi", }, }, }, @@ -1257,31 +1257,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_lqi", }, }, }, @@ -1301,31 +1301,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", - "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", - "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_control_outlet_identifybutton", + "switch.ikea_of_sweden_tradfri_control_outlet_switch", + "sensor.ikea_of_sweden_tradfri_control_outlet_rssi", + "sensor.ikea_of_sweden_tradfri_control_outlet_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.ikea_of_sweden_tradfri_control_outlet_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_lqi", }, }, }, @@ -1345,37 +1345,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_motion_sensor_77665544_identify", - "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", - "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", - "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_motion_sensor_identifybutton", + "sensor.ikea_of_sweden_tradfri_motion_sensor_battery", + "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_motion", + "sensor.ikea_of_sweden_tradfri_motion_sensor_rssi", + "sensor.ikea_of_sweden_tradfri_motion_sensor_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_motion_sensor_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_motion_sensor_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Motion", - DEV_SIG_ENT_MAP_ID: "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_motion", }, }, }, @@ -1395,31 +1395,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0102"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_on_off_switch_77665544_identify", - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_on_off_switch_identifybutton", + "sensor.ikea_of_sweden_tradfri_on_off_switch_battery", + "sensor.ikea_of_sweden_tradfri_on_off_switch_rssi", + "sensor.ikea_of_sweden_tradfri_on_off_switch_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_on_off_switch_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_on_off_switch_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_lqi", }, }, }, @@ -1439,31 +1439,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_remote_control_77665544_identify", - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_remote_control_identifybutton", + "sensor.ikea_of_sweden_tradfri_remote_control_battery", + "sensor.ikea_of_sweden_tradfri_remote_control_rssi", + "sensor.ikea_of_sweden_tradfri_remote_control_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_remote_control_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_remote_control_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_lqi", }, }, }, @@ -1490,25 +1490,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", - "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_signal_repeater_identifybutton", + "sensor.ikea_of_sweden_tradfri_signal_repeater_rssi", + "sensor.ikea_of_sweden_tradfri_signal_repeater_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_lqi", }, }, }, @@ -1528,31 +1528,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_wireless_dimmer_77665544_identify", - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_wireless_dimmer_identifybutton", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_battery", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_rssi", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_wireless_dimmer_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_wireless_dimmer_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_lqi", }, }, }, @@ -1579,43 +1579,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ - "button.jasco_products_45852_77665544_identify", - "sensor.jasco_products_45852_77665544_smartenergy_metering", - "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", - "light.jasco_products_45852_77665544_level_on_off", - "sensor.jasco_products_45852_77665544_basic_rssi", - "sensor.jasco_products_45852_77665544_basic_lqi", + "button.jasco_products_45852_identifybutton", + "sensor.jasco_products_45852_smartenergymetering", + "sensor.jasco_products_45852_smartenergysummation", + "light.jasco_products_45852_light", + "sensor.jasco_products_45852_rssi", + "sensor.jasco_products_45852_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45852_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.jasco_products_45852_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_lqi", }, }, }, @@ -1642,43 +1642,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.jasco_products_45856_77665544_identify", - "light.jasco_products_45856_77665544_on_off", - "sensor.jasco_products_45856_77665544_smartenergy_metering", - "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", - "sensor.jasco_products_45856_77665544_basic_rssi", - "sensor.jasco_products_45856_77665544_basic_lqi", + "button.jasco_products_45856_identifybutton", + "light.jasco_products_45856_light", + "sensor.jasco_products_45856_smartenergymetering", + "sensor.jasco_products_45856_smartenergysummation", + "sensor.jasco_products_45856_rssi", + "sensor.jasco_products_45856_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45856_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.jasco_products_45856_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_lqi", }, }, }, @@ -1705,43 +1705,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ - "button.jasco_products_45857_77665544_identify", - "light.jasco_products_45857_77665544_level_on_off", - "sensor.jasco_products_45857_77665544_smartenergy_metering", - "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", - "sensor.jasco_products_45857_77665544_basic_rssi", - "sensor.jasco_products_45857_77665544_basic_lqi", + "button.jasco_products_45857_identifybutton", + "light.jasco_products_45857_light", + "sensor.jasco_products_45857_smartenergymetering", + "sensor.jasco_products_45857_smartenergysummation", + "sensor.jasco_products_45857_rssi", + "sensor.jasco_products_45857_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45857_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.jasco_products_45857_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_lqi", }, }, }, @@ -1761,49 +1761,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.keen_home_inc_sv02_610_mp_1_3_77665544_identify", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", - "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_rssi", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_lqi", + "button.keen_home_inc_sv02_610_mp_1_3_identifybutton", + "sensor.keen_home_inc_sv02_610_mp_1_3_battery", + "sensor.keen_home_inc_sv02_610_mp_1_3_pressure", + "sensor.keen_home_inc_sv02_610_mp_1_3_temperature", + "cover.keen_home_inc_sv02_610_mp_1_3_keenvent", + "sensor.keen_home_inc_sv02_610_mp_1_3_rssi", + "sensor.keen_home_inc_sv02_610_mp_1_3_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_610_mp_1_3_keenvent", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_lqi", }, }, }, @@ -1823,49 +1823,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.keen_home_inc_sv02_612_mp_1_2_77665544_identify", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", - "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_rssi", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_lqi", + "button.keen_home_inc_sv02_612_mp_1_2_identifybutton", + "sensor.keen_home_inc_sv02_612_mp_1_2_battery", + "sensor.keen_home_inc_sv02_612_mp_1_2_pressure", + "sensor.keen_home_inc_sv02_612_mp_1_2_temperature", + "cover.keen_home_inc_sv02_612_mp_1_2_keenvent", + "sensor.keen_home_inc_sv02_612_mp_1_2_rssi", + "sensor.keen_home_inc_sv02_612_mp_1_2_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_2_keenvent", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_lqi", }, }, }, @@ -1885,49 +1885,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.keen_home_inc_sv02_612_mp_1_3_77665544_identify", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", - "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_rssi", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_lqi", + "button.keen_home_inc_sv02_612_mp_1_3_identifybutton", + "sensor.keen_home_inc_sv02_612_mp_1_3_battery", + "sensor.keen_home_inc_sv02_612_mp_1_3_pressure", + "sensor.keen_home_inc_sv02_612_mp_1_3_temperature", + "cover.keen_home_inc_sv02_612_mp_1_3_keenvent", + "sensor.keen_home_inc_sv02_612_mp_1_3_rssi", + "sensor.keen_home_inc_sv02_612_mp_1_3_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_3_keenvent", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_lqi", }, }, }, @@ -1947,37 +1947,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", - "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", - "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", - "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_rssi", - "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_lqi", + "button.king_of_fans_inc_hbuniversalcfremote_identifybutton", + "light.king_of_fans_inc_hbuniversalcfremote_light", + "fan.king_of_fans_inc_hbuniversalcfremote_fan", + "sensor.king_of_fans_inc_hbuniversalcfremote_rssi", + "sensor.king_of_fans_inc_hbuniversalcfremote_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.king_of_fans_inc_hbuniversalcfremote_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_lqi", }, ("fan", "00:11:22:33:44:55:66:77-1-514"): { DEV_SIG_CHANNELS: ["fan"], DEV_SIG_ENT_MAP_CLASS: "ZhaFan", - DEV_SIG_ENT_MAP_ID: "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", + DEV_SIG_ENT_MAP_ID: "fan.king_of_fans_inc_hbuniversalcfremote_fan", }, }, }, @@ -1997,31 +1997,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0300"], DEV_SIG_ENTITIES: [ - "button.lds_zbt_cctswitch_d0001_77665544_identify", - "sensor.lds_zbt_cctswitch_d0001_77665544_power", - "sensor.lds_zbt_cctswitch_d0001_77665544_basic_rssi", - "sensor.lds_zbt_cctswitch_d0001_77665544_basic_lqi", + "button.lds_zbt_cctswitch_d0001_identifybutton", + "sensor.lds_zbt_cctswitch_d0001_battery", + "sensor.lds_zbt_cctswitch_d0001_rssi", + "sensor.lds_zbt_cctswitch_d0001_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_lqi", }, }, }, @@ -2041,31 +2041,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_a19_rgbw_77665544_identify", - "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", - "sensor.ledvance_a19_rgbw_77665544_basic_rssi", - "sensor.ledvance_a19_rgbw_77665544_basic_lqi", + "button.ledvance_a19_rgbw_identifybutton", + "light.ledvance_a19_rgbw_light", + "sensor.ledvance_a19_rgbw_rssi", + "sensor.ledvance_a19_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_lqi", }, }, }, @@ -2085,31 +2085,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_flex_rgbw_77665544_identify", - "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", - "sensor.ledvance_flex_rgbw_77665544_basic_rssi", - "sensor.ledvance_flex_rgbw_77665544_basic_lqi", + "button.ledvance_flex_rgbw_identifybutton", + "light.ledvance_flex_rgbw_light", + "sensor.ledvance_flex_rgbw_rssi", + "sensor.ledvance_flex_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_lqi", }, }, }, @@ -2129,31 +2129,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_plug_77665544_identify", - "switch.ledvance_plug_77665544_on_off", - "sensor.ledvance_plug_77665544_basic_rssi", - "sensor.ledvance_plug_77665544_basic_lqi", + "button.ledvance_plug_identifybutton", + "switch.ledvance_plug_switch", + "sensor.ledvance_plug_rssi", + "sensor.ledvance_plug_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.ledvance_plug_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.ledvance_plug_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_lqi", }, }, }, @@ -2173,31 +2173,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_rt_rgbw_77665544_identify", - "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", - "sensor.ledvance_rt_rgbw_77665544_basic_rssi", - "sensor.ledvance_rt_rgbw_77665544_basic_lqi", + "button.ledvance_rt_rgbw_identifybutton", + "light.ledvance_rt_rgbw_light", + "sensor.ledvance_rt_rgbw_rssi", + "sensor.ledvance_rt_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_lqi", }, }, }, @@ -2238,91 +2238,91 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_plug_maus01_77665544_identify", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_apparent_power", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_ac_frequency", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_power_factor", - "sensor.lumi_lumi_plug_maus01_77665544_analog_input", - "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", - "binary_sensor.lumi_lumi_plug_maus01_77665544_binary_input", - "switch.lumi_lumi_plug_maus01_77665544_on_off", - "sensor.lumi_lumi_plug_maus01_77665544_basic_rssi", - "sensor.lumi_lumi_plug_maus01_77665544_basic_lqi", - "sensor.lumi_lumi_plug_maus01_77665544_device_temperature", + "button.lumi_lumi_plug_maus01_identifybutton", + "sensor.lumi_lumi_plug_maus01_electricalmeasurement", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementapparentpower", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmscurrent", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmsvoltage", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementfrequency", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementpowerfactor", + "sensor.lumi_lumi_plug_maus01_analoginput", + "sensor.lumi_lumi_plug_maus01_analoginput_2", + "binary_sensor.lumi_lumi_plug_maus01_binaryinput", + "switch.lumi_lumi_plug_maus01_switch", + "sensor.lumi_lumi_plug_maus01_rssi", + "sensor.lumi_lumi_plug_maus01_lqi", + "sensor.lumi_lumi_plug_maus01_devicetemperature", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.lumi_lumi_plug_maus01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.lumi_lumi_plug_maus01_switch", }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_device_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_devicetemperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-2-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_analoginput", }, ("sensor", "00:11:22:33:44:55:66:77-3-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_analoginput_2", }, ("binary_sensor", "00:11:22:33:44:55:66:77-100-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_plug_maus01_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_plug_maus01_binaryinput", }, }, }, @@ -2349,79 +2349,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_relay_c2acn01_77665544_identify", - "light.lumi_lumi_relay_c2acn01_77665544_on_off", - "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_apparent_power", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_ac_frequency", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_power_factor", - "sensor.lumi_lumi_relay_c2acn01_77665544_basic_rssi", - "sensor.lumi_lumi_relay_c2acn01_77665544_basic_lqi", - "sensor.lumi_lumi_relay_c2acn01_77665544_device_temperature", + "button.lumi_lumi_relay_c2acn01_identifybutton", + "light.lumi_lumi_relay_c2acn01_light", + "light.lumi_lumi_relay_c2acn01_light_2", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurement", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementapparentpower", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmscurrent", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmsvoltage", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementfrequency", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementpowerfactor", + "sensor.lumi_lumi_relay_c2acn01_rssi", + "sensor.lumi_lumi_relay_c2acn01_lqi", + "sensor.lumi_lumi_relay_c2acn01_devicetemperature", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_light", }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_device_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_devicetemperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_lqi", }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_light_2", }, }, }, @@ -2455,31 +2455,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b186acn01_77665544_identify", - "sensor.lumi_lumi_remote_b186acn01_77665544_power", - "sensor.lumi_lumi_remote_b186acn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b186acn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b186acn01_identifybutton", + "sensor.lumi_lumi_remote_b186acn01_battery", + "sensor.lumi_lumi_remote_b186acn01_rssi", + "sensor.lumi_lumi_remote_b186acn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_lqi", }, }, }, @@ -2513,31 +2513,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b286acn01_77665544_identify", - "sensor.lumi_lumi_remote_b286acn01_77665544_power", - "sensor.lumi_lumi_remote_b286acn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b286acn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b286acn01_identifybutton", + "sensor.lumi_lumi_remote_b286acn01_battery", + "sensor.lumi_lumi_remote_b286acn01_rssi", + "sensor.lumi_lumi_remote_b286acn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_lqi", }, }, }, @@ -2592,25 +2592,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b286opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b286opcn01_identifybutton", + "sensor.lumi_lumi_remote_b286opcn01_rssi", + "sensor.lumi_lumi_remote_b286opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_lqi", }, }, }, @@ -2665,25 +2665,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b486opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b486opcn01_identifybutton", + "sensor.lumi_lumi_remote_b486opcn01_rssi", + "sensor.lumi_lumi_remote_b486opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_lqi", }, }, }, @@ -2703,25 +2703,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b686opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b686opcn01_identifybutton", + "sensor.lumi_lumi_remote_b686opcn01_rssi", + "sensor.lumi_lumi_remote_b686opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_lqi", }, }, }, @@ -2776,25 +2776,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b686opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b686opcn01_identifybutton", + "sensor.lumi_lumi_remote_b686opcn01_rssi", + "sensor.lumi_lumi_remote_b686opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_lqi", }, }, }, @@ -2814,31 +2814,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], DEV_SIG_ENTITIES: [ - "light.lumi_lumi_router_77665544_on_off", - "binary_sensor.lumi_lumi_router_77665544_on_off", - "sensor.lumi_lumi_router_77665544_basic_rssi", - "sensor.lumi_lumi_router_77665544_basic_lqi", + "light.lumi_lumi_router_light", + "binary_sensor.lumi_lumi_router_opening", + "sensor.lumi_lumi_router_rssi", + "sensor.lumi_lumi_router_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", }, }, }, @@ -2858,31 +2858,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], DEV_SIG_ENTITIES: [ - "light.lumi_lumi_router_77665544_on_off", - "binary_sensor.lumi_lumi_router_77665544_on_off", - "sensor.lumi_lumi_router_77665544_basic_rssi", - "sensor.lumi_lumi_router_77665544_basic_lqi", + "light.lumi_lumi_router_light", + "binary_sensor.lumi_lumi_router_opening", + "sensor.lumi_lumi_router_rssi", + "sensor.lumi_lumi_router_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", }, }, }, @@ -2902,31 +2902,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], DEV_SIG_ENTITIES: [ - "light.lumi_lumi_router_77665544_on_off", - "binary_sensor.lumi_lumi_router_77665544_on_off", - "sensor.lumi_lumi_router_77665544_basic_rssi", - "sensor.lumi_lumi_router_77665544_basic_lqi", + "light.lumi_lumi_router_light", + "binary_sensor.lumi_lumi_router_opening", + "sensor.lumi_lumi_router_rssi", + "sensor.lumi_lumi_router_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", }, }, }, @@ -2946,31 +2946,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sen_ill_mgl01_77665544_identify", - "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", - "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_rssi", - "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_lqi", + "button.lumi_lumi_sen_ill_mgl01_identifybutton", + "sensor.lumi_lumi_sen_ill_mgl01_illuminance", + "sensor.lumi_lumi_sen_ill_mgl01_rssi", + "sensor.lumi_lumi_sen_ill_mgl01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_lqi", }, }, }, @@ -3004,31 +3004,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_86sw1_77665544_identify", - "sensor.lumi_lumi_sensor_86sw1_77665544_power", - "sensor.lumi_lumi_sensor_86sw1_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_86sw1_77665544_basic_lqi", + "button.lumi_lumi_sensor_86sw1_identifybutton", + "sensor.lumi_lumi_sensor_86sw1_battery", + "sensor.lumi_lumi_sensor_86sw1_rssi", + "sensor.lumi_lumi_sensor_86sw1_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_lqi", }, }, }, @@ -3062,31 +3062,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_cube_aqgl01_77665544_identify", - "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", - "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_lqi", + "button.lumi_lumi_sensor_cube_aqgl01_identifybutton", + "sensor.lumi_lumi_sensor_cube_aqgl01_battery", + "sensor.lumi_lumi_sensor_cube_aqgl01_rssi", + "sensor.lumi_lumi_sensor_cube_aqgl01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_lqi", }, }, }, @@ -3120,43 +3120,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_ht_77665544_identify", - "sensor.lumi_lumi_sensor_ht_77665544_power", - "sensor.lumi_lumi_sensor_ht_77665544_temperature", - "sensor.lumi_lumi_sensor_ht_77665544_humidity", - "sensor.lumi_lumi_sensor_ht_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_ht_77665544_basic_lqi", + "button.lumi_lumi_sensor_ht_identifybutton", + "sensor.lumi_lumi_sensor_ht_battery", + "sensor.lumi_lumi_sensor_ht_temperature", + "sensor.lumi_lumi_sensor_ht_humidity", + "sensor.lumi_lumi_sensor_ht_rssi", + "sensor.lumi_lumi_sensor_ht_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { DEV_SIG_CHANNELS: ["humidity"], DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_humidity", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_humidity", }, }, }, @@ -3176,37 +3176,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_magnet_77665544_identify", - "sensor.lumi_lumi_sensor_magnet_77665544_power", - "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", - "sensor.lumi_lumi_sensor_magnet_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_magnet_77665544_basic_lqi", + "button.lumi_lumi_sensor_magnet_identifybutton", + "sensor.lumi_lumi_sensor_magnet_battery", + "binary_sensor.lumi_lumi_sensor_magnet_opening", + "sensor.lumi_lumi_sensor_magnet_rssi", + "sensor.lumi_lumi_sensor_magnet_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_opening", }, }, }, @@ -3226,37 +3226,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_magnet_aq2_77665544_identify", - "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", - "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", - "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_lqi", + "button.lumi_lumi_sensor_magnet_aq2_identifybutton", + "sensor.lumi_lumi_sensor_magnet_aq2_battery", + "binary_sensor.lumi_lumi_sensor_magnet_aq2_opening", + "sensor.lumi_lumi_sensor_magnet_aq2_rssi", + "sensor.lumi_lumi_sensor_magnet_aq2_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_aq2_opening", }, }, }, @@ -3276,49 +3276,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_motion_aq2_77665544_identify", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", - "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", - "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_lqi", + "button.lumi_lumi_sensor_motion_aq2_identifybutton", + "sensor.lumi_lumi_sensor_motion_aq2_battery", + "sensor.lumi_lumi_sensor_motion_aq2_illuminance", + "binary_sensor.lumi_lumi_sensor_motion_aq2_occupancy", + "binary_sensor.lumi_lumi_sensor_motion_aq2_iaszone", + "sensor.lumi_lumi_sensor_motion_aq2_rssi", + "sensor.lumi_lumi_sensor_motion_aq2_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1030"): { DEV_SIG_CHANNELS: ["occupancy"], DEV_SIG_ENT_MAP_CLASS: "Occupancy", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_occupancy", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_lqi", }, }, }, @@ -3338,37 +3338,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_smoke_77665544_identify", - "sensor.lumi_lumi_sensor_smoke_77665544_power", - "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", - "sensor.lumi_lumi_sensor_smoke_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_smoke_77665544_basic_lqi", + "button.lumi_lumi_sensor_smoke_identifybutton", + "sensor.lumi_lumi_sensor_smoke_battery", + "binary_sensor.lumi_lumi_sensor_smoke_iaszone", + "sensor.lumi_lumi_sensor_smoke_rssi", + "sensor.lumi_lumi_sensor_smoke_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_smoke_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_lqi", }, }, }, @@ -3388,31 +3388,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_switch_77665544_identify", - "sensor.lumi_lumi_sensor_switch_77665544_power", - "sensor.lumi_lumi_sensor_switch_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_switch_77665544_basic_lqi", + "button.lumi_lumi_sensor_switch_identifybutton", + "sensor.lumi_lumi_sensor_switch_battery", + "sensor.lumi_lumi_sensor_switch_rssi", + "sensor.lumi_lumi_sensor_switch_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_lqi", }, }, }, @@ -3432,25 +3432,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", - "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_lqi", + "sensor.lumi_lumi_sensor_switch_aq2_battery", + "sensor.lumi_lumi_sensor_switch_aq2_rssi", + "sensor.lumi_lumi_sensor_switch_aq2_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_lqi", }, }, }, @@ -3470,25 +3470,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", - "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_lqi", + "sensor.lumi_lumi_sensor_switch_aq3_battery", + "sensor.lumi_lumi_sensor_switch_aq3_rssi", + "sensor.lumi_lumi_sensor_switch_aq3_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_lqi", }, }, }, @@ -3508,43 +3508,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_wleak_aq1_77665544_identify", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", - "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_lqi", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_device_temperature", + "button.lumi_lumi_sensor_wleak_aq1_identifybutton", + "sensor.lumi_lumi_sensor_wleak_aq1_battery", + "binary_sensor.lumi_lumi_sensor_wleak_aq1_iaszone", + "sensor.lumi_lumi_sensor_wleak_aq1_rssi", + "sensor.lumi_lumi_sensor_wleak_aq1_lqi", + "sensor.lumi_lumi_sensor_wleak_aq1_devicetemperature", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_wleak_aq1_iaszone", }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_device_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_devicetemperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_lqi", }, }, }, @@ -3571,43 +3571,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_vibration_aq1_77665544_identify", - "sensor.lumi_lumi_vibration_aq1_77665544_power", - "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", - "lock.lumi_lumi_vibration_aq1_77665544_door_lock", - "sensor.lumi_lumi_vibration_aq1_77665544_basic_rssi", - "sensor.lumi_lumi_vibration_aq1_77665544_basic_lqi", + "button.lumi_lumi_vibration_aq1_identifybutton", + "sensor.lumi_lumi_vibration_aq1_battery", + "binary_sensor.lumi_lumi_vibration_aq1_iaszone", + "lock.lumi_lumi_vibration_aq1_doorlock", + "sensor.lumi_lumi_vibration_aq1_rssi", + "sensor.lumi_lumi_vibration_aq1_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_vibration_aq1_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_lqi", }, ("lock", "00:11:22:33:44:55:66:77-1-257"): { DEV_SIG_CHANNELS: ["door_lock"], DEV_SIG_ENT_MAP_CLASS: "ZhaDoorLock", - DEV_SIG_ENT_MAP_ID: "lock.lumi_lumi_vibration_aq1_77665544_door_lock", + DEV_SIG_ENT_MAP_ID: "lock.lumi_lumi_vibration_aq1_doorlock", }, }, }, @@ -3627,49 +3627,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_weather_77665544_identify", - "sensor.lumi_lumi_weather_77665544_power", - "sensor.lumi_lumi_weather_77665544_pressure", - "sensor.lumi_lumi_weather_77665544_temperature", - "sensor.lumi_lumi_weather_77665544_humidity", - "sensor.lumi_lumi_weather_77665544_basic_rssi", - "sensor.lumi_lumi_weather_77665544_basic_lqi", + "button.lumi_lumi_weather_identifybutton", + "sensor.lumi_lumi_weather_battery", + "sensor.lumi_lumi_weather_pressure", + "sensor.lumi_lumi_weather_temperature", + "sensor.lumi_lumi_weather_humidity", + "sensor.lumi_lumi_weather_rssi", + "sensor.lumi_lumi_weather_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { DEV_SIG_CHANNELS: ["humidity"], DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_humidity", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_humidity", }, }, }, @@ -3689,37 +3689,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.nyce_3010_77665544_identify", - "sensor.nyce_3010_77665544_power", - "binary_sensor.nyce_3010_77665544_ias_zone", - "sensor.nyce_3010_77665544_basic_rssi", - "sensor.nyce_3010_77665544_basic_lqi", + "button.nyce_3010_identifybutton", + "sensor.nyce_3010_battery", + "binary_sensor.nyce_3010_iaszone", + "sensor.nyce_3010_rssi", + "sensor.nyce_3010_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3010_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3010_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.nyce_3010_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.nyce_3010_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_lqi", }, }, }, @@ -3739,37 +3739,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.nyce_3014_77665544_identify", - "sensor.nyce_3014_77665544_power", - "binary_sensor.nyce_3014_77665544_ias_zone", - "sensor.nyce_3014_77665544_basic_rssi", - "sensor.nyce_3014_77665544_basic_lqi", + "button.nyce_3014_identifybutton", + "sensor.nyce_3014_battery", + "binary_sensor.nyce_3014_iaszone", + "sensor.nyce_3014_rssi", + "sensor.nyce_3014_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3014_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3014_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.nyce_3014_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.nyce_3014_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_lqi", }, }, }, @@ -3832,31 +3832,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_a19_rgbw_77665544_identify", - "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", - "sensor.osram_lightify_a19_rgbw_77665544_basic_rssi", - "sensor.osram_lightify_a19_rgbw_77665544_basic_lqi", + "button.osram_lightify_a19_rgbw_identifybutton", + "light.osram_lightify_a19_rgbw_light", + "sensor.osram_lightify_a19_rgbw_rssi", + "sensor.osram_lightify_a19_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.osram_lightify_a19_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_lqi", }, }, }, @@ -3876,31 +3876,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_dimming_switch_77665544_identify", - "sensor.osram_lightify_dimming_switch_77665544_power", - "sensor.osram_lightify_dimming_switch_77665544_basic_rssi", - "sensor.osram_lightify_dimming_switch_77665544_basic_lqi", + "button.osram_lightify_dimming_switch_identifybutton", + "sensor.osram_lightify_dimming_switch_battery", + "sensor.osram_lightify_dimming_switch_rssi", + "sensor.osram_lightify_dimming_switch_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_lqi", }, }, }, @@ -3920,31 +3920,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_flex_rgbw_77665544_identify", - "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", - "sensor.osram_lightify_flex_rgbw_77665544_basic_rssi", - "sensor.osram_lightify_flex_rgbw_77665544_basic_lqi", + "button.osram_lightify_flex_rgbw_identifybutton", + "light.osram_lightify_flex_rgbw_light", + "sensor.osram_lightify_flex_rgbw_rssi", + "sensor.osram_lightify_flex_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.osram_lightify_flex_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_lqi", }, }, }, @@ -3964,67 +3964,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_rt_tunable_white_77665544_identify", - "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_apparent_power", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_ac_frequency", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_power_factor", - "sensor.osram_lightify_rt_tunable_white_77665544_basic_rssi", - "sensor.osram_lightify_rt_tunable_white_77665544_basic_lqi", + "button.osram_lightify_rt_tunable_white_identifybutton", + "light.osram_lightify_rt_tunable_white_light", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurement", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementapparentpower", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmscurrent", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmsvoltage", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementfrequency", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementpowerfactor", + "sensor.osram_lightify_rt_tunable_white_rssi", + "sensor.osram_lightify_rt_tunable_white_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.osram_lightify_rt_tunable_white_light", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_lqi", }, }, }, @@ -4044,67 +4044,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_plug_01_77665544_identify", - "sensor.osram_plug_01_77665544_electrical_measurement", - "sensor.osram_plug_01_77665544_electrical_measurement_apparent_power", - "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", - "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", - "sensor.osram_plug_01_77665544_electrical_measurement_ac_frequency", - "sensor.osram_plug_01_77665544_electrical_measurement_power_factor", - "switch.osram_plug_01_77665544_on_off", - "sensor.osram_plug_01_77665544_basic_rssi", - "sensor.osram_plug_01_77665544_basic_lqi", + "button.osram_plug_01_identifybutton", + "sensor.osram_plug_01_electricalmeasurement", + "sensor.osram_plug_01_electricalmeasurementapparentpower", + "sensor.osram_plug_01_electricalmeasurementrmscurrent", + "sensor.osram_plug_01_electricalmeasurementrmsvoltage", + "sensor.osram_plug_01_electricalmeasurementfrequency", + "sensor.osram_plug_01_electricalmeasurementpowerfactor", + "switch.osram_plug_01_switch", + "sensor.osram_plug_01_rssi", + "sensor.osram_plug_01_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.osram_plug_01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.osram_plug_01_switch", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_lqi", }, }, }, @@ -4185,25 +4185,25 @@ DEVICES = [ "6:0x0300", ], DEV_SIG_ENTITIES: [ - "sensor.osram_switch_4x_lightify_77665544_power", - "sensor.osram_switch_4x_lightify_77665544_basic_rssi", - "sensor.osram_switch_4x_lightify_77665544_basic_lqi", + "sensor.osram_switch_4x_lightify_battery", + "sensor.osram_switch_4x_lightify_rssi", + "sensor.osram_switch_4x_lightify_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_lqi", }, }, }, @@ -4230,37 +4230,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "2:0x0019"], DEV_SIG_ENTITIES: [ - "button.philips_rwl020_77665544_identify", - "sensor.philips_rwl020_77665544_power", - "binary_sensor.philips_rwl020_77665544_binary_input", - "sensor.philips_rwl020_77665544_basic_rssi", - "sensor.philips_rwl020_77665544_basic_lqi", + "button.philips_rwl020_identifybutton", + "sensor.philips_rwl020_battery", + "binary_sensor.philips_rwl020_binaryinput", + "sensor.philips_rwl020_rssi", + "sensor.philips_rwl020_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_rwl020_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_rwl020_binaryinput", }, ("button", "00:11:22:33:44:55:66:77-2-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-2-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_battery", }, }, }, @@ -4280,43 +4280,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.samjin_button_77665544_identify", - "sensor.samjin_button_77665544_power", - "sensor.samjin_button_77665544_temperature", - "binary_sensor.samjin_button_77665544_ias_zone", - "sensor.samjin_button_77665544_basic_rssi", - "sensor.samjin_button_77665544_basic_lqi", + "button.samjin_button_identifybutton", + "sensor.samjin_button_battery", + "sensor.samjin_button_temperature", + "binary_sensor.samjin_button_iaszone", + "sensor.samjin_button_rssi", + "sensor.samjin_button_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_button_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_button_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_button_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.samjin_button_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_lqi", }, }, }, @@ -4336,43 +4336,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.samjin_multi_77665544_identify", - "sensor.samjin_multi_77665544_power", - "sensor.samjin_multi_77665544_temperature", - "binary_sensor.samjin_multi_77665544_ias_zone", - "sensor.samjin_multi_77665544_basic_rssi", - "sensor.samjin_multi_77665544_basic_lqi", + "button.samjin_multi_identifybutton", + "sensor.samjin_multi_battery", + "sensor.samjin_multi_temperature", + "binary_sensor.samjin_multi_iaszone", + "sensor.samjin_multi_rssi", + "sensor.samjin_multi_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_multi_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.samjin_multi_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_lqi", }, }, }, @@ -4392,43 +4392,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.samjin_water_77665544_identify", - "sensor.samjin_water_77665544_power", - "sensor.samjin_water_77665544_temperature", - "binary_sensor.samjin_water_77665544_ias_zone", - "sensor.samjin_water_77665544_basic_rssi", - "sensor.samjin_water_77665544_basic_lqi", + "button.samjin_water_identifybutton", + "sensor.samjin_water_battery", + "sensor.samjin_water_temperature", + "binary_sensor.samjin_water_iaszone", + "sensor.samjin_water_rssi", + "sensor.samjin_water_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_water_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_water_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_water_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.samjin_water_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_lqi", }, }, }, @@ -4448,67 +4448,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.securifi_ltd_unk_model_77665544_identify", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_apparent_power", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_ac_frequency", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_power_factor", - "switch.securifi_ltd_unk_model_77665544_on_off", - "sensor.securifi_ltd_unk_model_77665544_basic_rssi", - "sensor.securifi_ltd_unk_model_77665544_basic_lqi", + "button.securifi_ltd_unk_model_identifybutton", + "sensor.securifi_ltd_unk_model_electricalmeasurement", + "sensor.securifi_ltd_unk_model_electricalmeasurementapparentpower", + "sensor.securifi_ltd_unk_model_electricalmeasurementrmscurrent", + "sensor.securifi_ltd_unk_model_electricalmeasurementrmsvoltage", + "sensor.securifi_ltd_unk_model_electricalmeasurementfrequency", + "sensor.securifi_ltd_unk_model_electricalmeasurementpowerfactor", + "switch.securifi_ltd_unk_model_switch", + "sensor.securifi_ltd_unk_model_rssi", + "sensor.securifi_ltd_unk_model_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_switch", }, }, }, @@ -4528,43 +4528,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sercomm_corp_sz_dws04n_sf_77665544_identify", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", - "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_rssi", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_lqi", + "button.sercomm_corp_sz_dws04n_sf_identifybutton", + "sensor.sercomm_corp_sz_dws04n_sf_battery", + "sensor.sercomm_corp_sz_dws04n_sf_temperature", + "binary_sensor.sercomm_corp_sz_dws04n_sf_iaszone", + "sensor.sercomm_corp_sz_dws04n_sf_rssi", + "sensor.sercomm_corp_sz_dws04n_sf_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_dws04n_sf_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_lqi", }, }, }, @@ -4591,79 +4591,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.sercomm_corp_sz_esw01_77665544_identify", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_ac_frequency", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_power_factor", - "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", - "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", - "light.sercomm_corp_sz_esw01_77665544_on_off", - "sensor.sercomm_corp_sz_esw01_77665544_basic_rssi", - "sensor.sercomm_corp_sz_esw01_77665544_basic_lqi", + "button.sercomm_corp_sz_esw01_identifybutton", + "sensor.sercomm_corp_sz_esw01_electricalmeasurement", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementapparentpower", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmscurrent", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmsvoltage", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementfrequency", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementpowerfactor", + "sensor.sercomm_corp_sz_esw01_smartenergymetering", + "sensor.sercomm_corp_sz_esw01_smartenergysummation", + "light.sercomm_corp_sz_esw01_light", + "sensor.sercomm_corp_sz_esw01_rssi", + "sensor.sercomm_corp_sz_esw01_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sercomm_corp_sz_esw01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.sercomm_corp_sz_esw01_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_lqi", }, }, }, @@ -4683,49 +4683,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sercomm_corp_sz_pir04_77665544_identify", - "sensor.sercomm_corp_sz_pir04_77665544_power", - "sensor.sercomm_corp_sz_pir04_77665544_illuminance", - "sensor.sercomm_corp_sz_pir04_77665544_temperature", - "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", - "sensor.sercomm_corp_sz_pir04_77665544_basic_rssi", - "sensor.sercomm_corp_sz_pir04_77665544_basic_lqi", + "button.sercomm_corp_sz_pir04_identifybutton", + "sensor.sercomm_corp_sz_pir04_battery", + "sensor.sercomm_corp_sz_pir04_illuminance", + "sensor.sercomm_corp_sz_pir04_temperature", + "binary_sensor.sercomm_corp_sz_pir04_iaszone", + "sensor.sercomm_corp_sz_pir04_rssi", + "sensor.sercomm_corp_sz_pir04_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_pir04_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_lqi", }, }, }, @@ -4745,67 +4745,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sinope_technologies_rm3250zb_77665544_identify", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_apparent_power", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_ac_frequency", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_power_factor", - "switch.sinope_technologies_rm3250zb_77665544_on_off", - "sensor.sinope_technologies_rm3250zb_77665544_basic_rssi", - "sensor.sinope_technologies_rm3250zb_77665544_basic_lqi", + "button.sinope_technologies_rm3250zb_identifybutton", + "sensor.sinope_technologies_rm3250zb_electricalmeasurement", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementapparentpower", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmscurrent", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmsvoltage", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementfrequency", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementpowerfactor", + "switch.sinope_technologies_rm3250zb_switch", + "sensor.sinope_technologies_rm3250zb_rssi", + "sensor.sinope_technologies_rm3250zb_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_switch", }, }, }, @@ -4832,79 +4832,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sinope_technologies_th1123zb_77665544_identify", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_apparent_power", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_ac_frequency", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_power_factor", - "sensor.sinope_technologies_th1123zb_77665544_temperature", - "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", - "climate.sinope_technologies_th1123zb_77665544_thermostat", - "sensor.sinope_technologies_th1123zb_77665544_basic_rssi", - "sensor.sinope_technologies_th1123zb_77665544_basic_lqi", + "button.sinope_technologies_th1123zb_identifybutton", + "sensor.sinope_technologies_th1123zb_electricalmeasurement", + "sensor.sinope_technologies_th1123zb_electricalmeasurementapparentpower", + "sensor.sinope_technologies_th1123zb_electricalmeasurementrmscurrent", + "sensor.sinope_technologies_th1123zb_electricalmeasurementrmsvoltage", + "sensor.sinope_technologies_th1123zb_electricalmeasurementfrequency", + "sensor.sinope_technologies_th1123zb_electricalmeasurementpowerfactor", + "sensor.sinope_technologies_th1123zb_temperature", + "sensor.sinope_technologies_th1123zb_sinopehvacaction", + "climate.sinope_technologies_th1123zb_thermostat", + "sensor.sinope_technologies_th1123zb_rssi", + "sensor.sinope_technologies_th1123zb_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_identifybutton", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "Thermostat", - DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1123zb_77665544_thermostat", + DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1123zb_thermostat", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_sinopehvacaction", }, }, }, @@ -4931,79 +4931,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sinope_technologies_th1124zb_77665544_identify", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_apparent_power", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_ac_frequency", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_power_factor", - "sensor.sinope_technologies_th1124zb_77665544_temperature", - "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", - "climate.sinope_technologies_th1124zb_77665544_thermostat", - "sensor.sinope_technologies_th1124zb_77665544_basic_rssi", - "sensor.sinope_technologies_th1124zb_77665544_basic_lqi", + "button.sinope_technologies_th1124zb_identifybutton", + "sensor.sinope_technologies_th1124zb_electricalmeasurement", + "sensor.sinope_technologies_th1124zb_electricalmeasurementapparentpower", + "sensor.sinope_technologies_th1124zb_electricalmeasurementrmscurrent", + "sensor.sinope_technologies_th1124zb_electricalmeasurementrmsvoltage", + "sensor.sinope_technologies_th1124zb_electricalmeasurementfrequency", + "sensor.sinope_technologies_th1124zb_electricalmeasurementpowerfactor", + "sensor.sinope_technologies_th1124zb_temperature", + "sensor.sinope_technologies_th1124zb_sinopehvacaction", + "climate.sinope_technologies_th1124zb_thermostat", + "sensor.sinope_technologies_th1124zb_rssi", + "sensor.sinope_technologies_th1124zb_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_identifybutton", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "Thermostat", - DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_77665544_thermostat", + DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_thermostat", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_sinopehvacaction", }, }, }, @@ -5023,73 +5023,73 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.smartthings_outletv4_77665544_identify", - "sensor.smartthings_outletv4_77665544_electrical_measurement", - "sensor.smartthings_outletv4_77665544_electrical_measurement_apparent_power", - "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", - "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", - "sensor.smartthings_outletv4_77665544_electrical_measurement_ac_frequency", - "sensor.smartthings_outletv4_77665544_electrical_measurement_power_factor", - "binary_sensor.smartthings_outletv4_77665544_binary_input", - "switch.smartthings_outletv4_77665544_on_off", - "sensor.smartthings_outletv4_77665544_basic_rssi", - "sensor.smartthings_outletv4_77665544_basic_lqi", + "button.smartthings_outletv4_identifybutton", + "sensor.smartthings_outletv4_electricalmeasurement", + "sensor.smartthings_outletv4_electricalmeasurementapparentpower", + "sensor.smartthings_outletv4_electricalmeasurementrmscurrent", + "sensor.smartthings_outletv4_electricalmeasurementrmsvoltage", + "sensor.smartthings_outletv4_electricalmeasurementfrequency", + "sensor.smartthings_outletv4_electricalmeasurementpowerfactor", + "binary_sensor.smartthings_outletv4_binaryinput", + "switch.smartthings_outletv4_switch", + "sensor.smartthings_outletv4_rssi", + "sensor.smartthings_outletv4_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_outletv4_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_outletv4_binaryinput", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_switch", }, }, }, @@ -5109,37 +5109,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.smartthings_tagv4_77665544_identify", - "device_tracker.smartthings_tagv4_77665544_power", - "binary_sensor.smartthings_tagv4_77665544_binary_input", - "sensor.smartthings_tagv4_77665544_basic_rssi", - "sensor.smartthings_tagv4_77665544_basic_lqi", + "button.smartthings_tagv4_identifybutton", + "device_tracker.smartthings_tagv4_devicescanner", + "binary_sensor.smartthings_tagv4_binaryinput", + "sensor.smartthings_tagv4_rssi", + "sensor.smartthings_tagv4_lqi", ], DEV_SIG_ENT_MAP: { ("device_tracker", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "ZHADeviceScannerEntity", - DEV_SIG_ENT_MAP_ID: "device_tracker.smartthings_tagv4_77665544_power", + DEV_SIG_ENT_MAP_ID: "device_tracker.smartthings_tagv4_devicescanner", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_tagv4_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_tagv4_binaryinput", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_lqi", }, }, }, @@ -5159,31 +5159,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.third_reality_inc_3rss007z_77665544_identify", - "switch.third_reality_inc_3rss007z_77665544_on_off", - "sensor.third_reality_inc_3rss007z_77665544_basic_rssi", - "sensor.third_reality_inc_3rss007z_77665544_basic_lqi", + "button.third_reality_inc_3rss007z_identifybutton", + "switch.third_reality_inc_3rss007z_switch", + "sensor.third_reality_inc_3rss007z_rssi", + "sensor.third_reality_inc_3rss007z_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss007z_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss007z_switch", }, }, }, @@ -5203,37 +5203,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.third_reality_inc_3rss008z_77665544_identify", - "sensor.third_reality_inc_3rss008z_77665544_power", - "switch.third_reality_inc_3rss008z_77665544_on_off", - "sensor.third_reality_inc_3rss008z_77665544_basic_rssi", - "sensor.third_reality_inc_3rss008z_77665544_basic_lqi", + "button.third_reality_inc_3rss008z_identifybutton", + "sensor.third_reality_inc_3rss008z_battery", + "switch.third_reality_inc_3rss008z_switch", + "sensor.third_reality_inc_3rss008z_rssi", + "sensor.third_reality_inc_3rss008z_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss008z_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss008z_switch", }, }, }, @@ -5253,43 +5253,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.visonic_mct_340_e_77665544_identify", - "sensor.visonic_mct_340_e_77665544_power", - "sensor.visonic_mct_340_e_77665544_temperature", - "binary_sensor.visonic_mct_340_e_77665544_ias_zone", - "sensor.visonic_mct_340_e_77665544_basic_rssi", - "sensor.visonic_mct_340_e_77665544_basic_lqi", + "button.visonic_mct_340_e_identifybutton", + "sensor.visonic_mct_340_e_battery", + "sensor.visonic_mct_340_e_temperature", + "binary_sensor.visonic_mct_340_e_iaszone", + "sensor.visonic_mct_340_e_rssi", + "sensor.visonic_mct_340_e_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.visonic_mct_340_e_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.visonic_mct_340_e_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_lqi", }, }, }, @@ -5309,43 +5309,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.zen_within_zen_01_77665544_identify", - "sensor.zen_within_zen_01_77665544_power", - "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", - "climate.zen_within_zen_01_77665544_fan_thermostat", - "sensor.zen_within_zen_01_77665544_basic_rssi", - "sensor.zen_within_zen_01_77665544_basic_lqi", + "button.zen_within_zen_01_identifybutton", + "sensor.zen_within_zen_01_battery", + "sensor.zen_within_zen_01_thermostathvacaction", + "climate.zen_within_zen_01_zenwithinthermostat", + "sensor.zen_within_zen_01_rssi", + "sensor.zen_within_zen_01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_identifybutton", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat", "fan"], DEV_SIG_ENT_MAP_CLASS: "ZenWithinThermostat", - DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_77665544_fan_thermostat", + DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_zenwithinthermostat", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_thermostathvacaction", }, }, }, @@ -5386,43 +5386,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", - "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_rssi", - "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_lqi", + "light.tyzb01_ns1ndbww_ts0004_light", + "light.tyzb01_ns1ndbww_ts0004_light_2", + "light.tyzb01_ns1ndbww_ts0004_light_3", + "light.tyzb01_ns1ndbww_ts0004_light_4", + "sensor.tyzb01_ns1ndbww_ts0004_rssi", + "sensor.tyzb01_ns1ndbww_ts0004_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_lqi", }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_2", }, ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_3", }, ("light", "00:11:22:33:44:55:66:77-4"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_4", }, }, }, @@ -5442,37 +5442,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.netvox_z308e3ed_77665544_identify", - "sensor.netvox_z308e3ed_77665544_power", - "binary_sensor.netvox_z308e3ed_77665544_ias_zone", - "sensor.netvox_z308e3ed_77665544_basic_rssi", - "sensor.netvox_z308e3ed_77665544_basic_lqi", + "button.netvox_z308e3ed_identifybutton", + "sensor.netvox_z308e3ed_battery", + "binary_sensor.netvox_z308e3ed_iaszone", + "sensor.netvox_z308e3ed_rssi", + "sensor.netvox_z308e3ed_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.netvox_z308e3ed_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.netvox_z308e3ed_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_lqi", }, }, }, @@ -5492,43 +5492,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sengled_e11_g13_77665544_identify", - "light.sengled_e11_g13_77665544_level_on_off", - "sensor.sengled_e11_g13_77665544_smartenergy_metering", - "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", - "sensor.sengled_e11_g13_77665544_basic_rssi", - "sensor.sengled_e11_g13_77665544_basic_lqi", + "button.sengled_e11_g13_identifybutton", + "light.sengled_e11_g13_light", + "sensor.sengled_e11_g13_smartenergymetering", + "sensor.sengled_e11_g13_smartenergysummation", + "sensor.sengled_e11_g13_rssi", + "sensor.sengled_e11_g13_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_lqi", }, }, }, @@ -5548,43 +5548,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sengled_e12_n14_77665544_identify", - "light.sengled_e12_n14_77665544_level_on_off", - "sensor.sengled_e12_n14_77665544_smartenergy_metering", - "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", - "sensor.sengled_e12_n14_77665544_basic_rssi", - "sensor.sengled_e12_n14_77665544_basic_lqi", + "button.sengled_e12_n14_identifybutton", + "light.sengled_e12_n14_light", + "sensor.sengled_e12_n14_smartenergymetering", + "sensor.sengled_e12_n14_smartenergysummation", + "sensor.sengled_e12_n14_rssi", + "sensor.sengled_e12_n14_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_lqi", }, }, }, @@ -5604,43 +5604,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sengled_z01_a19nae26_77665544_identify", - "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", - "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", - "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", - "sensor.sengled_z01_a19nae26_77665544_basic_rssi", - "sensor.sengled_z01_a19nae26_77665544_basic_lqi", + "button.sengled_z01_a19nae26_identifybutton", + "light.sengled_z01_a19nae26_light", + "sensor.sengled_z01_a19nae26_smartenergymetering", + "sensor.sengled_z01_a19nae26_smartenergysummation", + "sensor.sengled_z01_a19nae26_rssi", + "sensor.sengled_z01_a19nae26_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_lqi", }, }, }, @@ -5660,31 +5660,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.unk_manufacturer_unk_model_77665544_identify", - "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", - "sensor.unk_manufacturer_unk_model_77665544_basic_rssi", - "sensor.unk_manufacturer_unk_model_77665544_basic_lqi", + "button.unk_manufacturer_unk_model_identifybutton", + "cover.unk_manufacturer_unk_model_shade", + "sensor.unk_manufacturer_unk_model_rssi", + "sensor.unk_manufacturer_unk_model_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off", "shade"], DEV_SIG_ENT_MAP_CLASS: "Shade", - DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", + DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_shade", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_lqi", }, }, }, @@ -5809,139 +5809,139 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["232:0x0008"], DEV_SIG_ENTITIES: [ - "number.digi_xbee3_77665544_analog_output", - "number.digi_xbee3_77665544_analog_output_2", - "sensor.digi_xbee3_77665544_analog_input", - "sensor.digi_xbee3_77665544_analog_input_2", - "sensor.digi_xbee3_77665544_analog_input_3", - "sensor.digi_xbee3_77665544_analog_input_4", - "sensor.digi_xbee3_77665544_analog_input_5", - "switch.digi_xbee3_77665544_on_off", - "switch.digi_xbee3_77665544_on_off_2", - "switch.digi_xbee3_77665544_on_off_3", - "switch.digi_xbee3_77665544_on_off_4", - "switch.digi_xbee3_77665544_on_off_5", - "switch.digi_xbee3_77665544_on_off_6", - "switch.digi_xbee3_77665544_on_off_7", - "switch.digi_xbee3_77665544_on_off_8", - "switch.digi_xbee3_77665544_on_off_9", - "switch.digi_xbee3_77665544_on_off_10", - "switch.digi_xbee3_77665544_on_off_11", - "switch.digi_xbee3_77665544_on_off_12", - "switch.digi_xbee3_77665544_on_off_13", - "switch.digi_xbee3_77665544_on_off_14", - "switch.digi_xbee3_77665544_on_off_15", + "number.digi_xbee3_number", + "number.digi_xbee3_number_2", + "sensor.digi_xbee3_analoginput", + "sensor.digi_xbee3_analoginput_2", + "sensor.digi_xbee3_analoginput_3", + "sensor.digi_xbee3_analoginput_4", + "sensor.digi_xbee3_analoginput_5", + "switch.digi_xbee3_switch", + "switch.digi_xbee3_switch_2", + "switch.digi_xbee3_switch_3", + "switch.digi_xbee3_switch_4", + "switch.digi_xbee3_switch_5", + "switch.digi_xbee3_switch_6", + "switch.digi_xbee3_switch_7", + "switch.digi_xbee3_switch_8", + "switch.digi_xbee3_switch_9", + "switch.digi_xbee3_switch_10", + "switch.digi_xbee3_switch_11", + "switch.digi_xbee3_switch_12", + "switch.digi_xbee3_switch_13", + "switch.digi_xbee3_switch_14", + "switch.digi_xbee3_switch_15", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-208-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput", }, ("switch", "00:11:22:33:44:55:66:77-208-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch", }, ("sensor", "00:11:22:33:44:55:66:77-209-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_2", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_2", }, ("switch", "00:11:22:33:44:55:66:77-209-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_2", }, ("sensor", "00:11:22:33:44:55:66:77-210-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_3", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_3", }, ("switch", "00:11:22:33:44:55:66:77-210-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_3", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_3", }, ("sensor", "00:11:22:33:44:55:66:77-211-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_4", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_4", }, ("switch", "00:11:22:33:44:55:66:77-211-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_4", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_4", }, ("switch", "00:11:22:33:44:55:66:77-212-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_5", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_5", }, ("switch", "00:11:22:33:44:55:66:77-213-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_6", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_6", }, ("switch", "00:11:22:33:44:55:66:77-214-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_7", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_7", }, ("sensor", "00:11:22:33:44:55:66:77-215-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_5", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_5", }, ("switch", "00:11:22:33:44:55:66:77-215-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_8", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_8", }, ("switch", "00:11:22:33:44:55:66:77-216-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_9", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_9", }, ("switch", "00:11:22:33:44:55:66:77-217-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_10", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_10", }, ("number", "00:11:22:33:44:55:66:77-218-13"): { DEV_SIG_CHANNELS: ["analog_output"], DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output", + DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_number", }, ("switch", "00:11:22:33:44:55:66:77-218-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_11", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_11", }, ("switch", "00:11:22:33:44:55:66:77-219-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_12", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_12", }, ("number", "00:11:22:33:44:55:66:77-219-13"): { DEV_SIG_CHANNELS: ["analog_output"], DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output_2", + DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_number_2", }, ("switch", "00:11:22:33:44:55:66:77-220-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_13", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_13", }, ("switch", "00:11:22:33:44:55:66:77-221-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_14", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_14", }, ("switch", "00:11:22:33:44:55:66:77-222-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_15", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_15", }, }, }, @@ -5961,37 +5961,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "sensor.efektalab_ru_efekta_pws_77665544_power", - "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", - "sensor.efektalab_ru_efekta_pws_77665544_temperature", - "sensor.efektalab_ru_efekta_pws_77665544_basic_rssi", - "sensor.efektalab_ru_efekta_pws_77665544_basic_lqi", + "sensor.efektalab_ru_efekta_pws_battery", + "sensor.efektalab_ru_efekta_pws_soilmoisture", + "sensor.efektalab_ru_efekta_pws_temperature", + "sensor.efektalab_ru_efekta_pws_rssi", + "sensor.efektalab_ru_efekta_pws_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1032"): { DEV_SIG_CHANNELS: ["soil_moisture"], DEV_SIG_ENT_MAP_CLASS: "SoilMoisture", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_soilmoisture", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_lqi", }, }, }, From cdab725bf482b8d90211f16170317045c313bf63 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:18:15 +0200 Subject: [PATCH 318/820] Migrate Slimproto to new entity naming style (#74910) --- homeassistant/components/slimproto/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index 2f85aa4b9df..bcd538f57e8 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -74,6 +74,7 @@ async def async_setup_entry( class SlimProtoPlayer(MediaPlayerEntity): """Representation of MediaPlayerEntity from SlimProto Player.""" + _attr_has_entity_name = True _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -139,7 +140,6 @@ class SlimProtoPlayer(MediaPlayerEntity): @callback def update_attributes(self) -> None: """Handle player updates.""" - self._attr_name = self.player.name self._attr_volume_level = self.player.volume_level / 100 self._attr_media_position = self.player.elapsed_seconds self._attr_media_position_updated_at = utcnow() From 5f728b955e416332d98bb9f87550d84e9f1f979a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:19:13 +0200 Subject: [PATCH 319/820] Migrate Sonos to new entity naming style (#74909) --- .../components/sonos/binary_sensor.py | 4 ++-- homeassistant/components/sonos/entity.py | 1 + .../components/sonos/media_player.py | 1 - homeassistant/components/sonos/number.py | 3 +-- homeassistant/components/sonos/sensor.py | 6 +++--- homeassistant/components/sonos/switch.py | 20 ++++++++----------- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 4eaa75f92ae..e890c1c64a8 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -58,12 +58,12 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING + _attr_name = "Power" def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the power entity binary sensor.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-power" - self._attr_name = f"{self.speaker.zone_name} Power" async def _async_fallback_poll(self) -> None: """Poll the device for the current state.""" @@ -92,12 +92,12 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:microphone" + _attr_name = "Microphone" def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the microphone binary sensor entity.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-microphone" - self._attr_name = f"{self.speaker.zone_name} Microphone" async def _async_fallback_poll(self) -> None: """Handle polling when subscription fails.""" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index ba1f72cd56b..0955fb0e82d 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -26,6 +26,7 @@ class SonosEntity(Entity): """Representation of a Sonos entity.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, speaker: SonosSpeaker) -> None: """Initialize a SonosEntity.""" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4e7998c8e4e..1f57cafbf09 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -216,7 +216,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Initialize the media player entity.""" super().__init__(speaker) self._attr_unique_id = self.soco.uid - self._attr_name = self.speaker.zone_name async def async_added_to_hass(self) -> None: """Handle common setup when added to hass.""" diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 3b034423471..ccbcbc3c339 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -72,8 +72,7 @@ class SonosLevelEntity(SonosEntity, NumberEntity): """Initialize the level entity.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-{level_type}" - name_suffix = level_type.replace("_", " ").title() - self._attr_name = f"{self.speaker.zone_name} {name_suffix}" + self._attr_name = level_type.replace("_", " ").capitalize() self.level_type = level_type self._attr_native_min_value, self._attr_native_max_value = valid_range diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 380d1a3b9b6..8477e523a40 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -80,13 +80,13 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_name = "Battery" _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the battery sensor.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-battery" - self._attr_name = f"{self.speaker.zone_name} Battery" async def _async_fallback_poll(self) -> None: """Poll the device for the current state.""" @@ -108,13 +108,13 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:import" + _attr_name = "Audio input format" _attr_should_poll = True def __init__(self, speaker: SonosSpeaker, audio_format: str) -> None: """Initialize the audio input format sensor.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-audio-format" - self._attr_name = f"{self.speaker.zone_name} Audio Input Format" self._attr_native_value = audio_format def poll_state(self) -> None: @@ -137,7 +137,7 @@ class SonosFavoritesEntity(SensorEntity): _attr_entity_registry_enabled_default = False _attr_icon = "mdi:star" - _attr_name = "Sonos Favorites" + _attr_name = "Sonos favorites" _attr_native_unit_of_measurement = "items" _attr_should_poll = False diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 53911d85d3e..8dcf47fd0a2 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -69,13 +69,13 @@ POLL_REQUIRED = ( FRIENDLY_NAMES = { ATTR_CROSSFADE: "Crossfade", ATTR_LOUDNESS: "Loudness", - ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround Music Full Volume", - ATTR_NIGHT_SOUND: "Night Sound", - ATTR_SPEECH_ENHANCEMENT: "Speech Enhancement", + ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround music full volume", + ATTR_NIGHT_SOUND: "Night sound", + ATTR_SPEECH_ENHANCEMENT: "Speech enhancement", ATTR_STATUS_LIGHT: "Status Light", - ATTR_SUB_ENABLED: "Subwoofer Enabled", - ATTR_SURROUND_ENABLED: "Surround Enabled", - ATTR_TOUCH_CONTROLS: "Touch Controls", + ATTR_SUB_ENABLED: "Subwoofer enabled", + ATTR_SURROUND_ENABLED: "Surround enabled", + ATTR_TOUCH_CONTROLS: "Touch controls", } FEATURE_ICONS = { @@ -160,7 +160,7 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): self.feature_type = feature_type self.needs_coordinator = feature_type in COORDINATOR_FEATURES self._attr_entity_category = EntityCategory.CONFIG - self._attr_name = f"{speaker.zone_name} {FRIENDLY_NAMES[feature_type]}" + self._attr_name = FRIENDLY_NAMES[feature_type] self._attr_unique_id = f"{speaker.soco.uid}-{feature_type}" self._attr_icon = FEATURE_ICONS.get(feature_type) @@ -240,11 +240,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): @property def name(self) -> str: """Return the name of the sensor.""" - return "{} {} Alarm {}".format( - self.speaker.zone_name, - self.alarm.recurrence.title(), - str(self.alarm.start_time)[0:5], - ) + return f"{self.alarm.recurrence.capitalize()} alarm {str(self.alarm.start_time)[:5]}" async def _async_fallback_poll(self) -> None: """Call the central alarm polling method.""" From a19ab389fc47c1cb19081fff34bd0dd172dc9b97 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:29:50 -0600 Subject: [PATCH 320/820] Migrate Flu Near You to new entity naming style (#74918) --- homeassistant/components/flunearyou/sensor.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 8a232d3d103..f666e7412eb 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -42,12 +42,12 @@ SENSOR_TYPE_USER_TOTAL = "total" CDC_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_CDC_LEVEL, - name="CDC Level", + name="CDC level", icon="mdi:biohazard", ), SensorEntityDescription( key=SENSOR_TYPE_CDC_LEVEL2, - name="CDC Level 2", + name="CDC level 2", icon="mdi:biohazard", ), ) @@ -55,49 +55,49 @@ CDC_SENSOR_DESCRIPTIONS = ( USER_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_USER_CHICK, - name="Avian Flu Symptoms", + name="Avian flu symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_DENGUE, - name="Dengue Fever Symptoms", + name="Dengue fever symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_FLU, - name="Flu Symptoms", + name="Flu symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_LEPTO, - name="Leptospirosis Symptoms", + name="Leptospirosis symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_NO_SYMPTOMS, - name="No Symptoms", + name="No symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_SYMPTOMS, - name="Flu-like Symptoms", + name="Flu-like symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_TOTAL, - name="Total Symptoms", + name="Total symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, @@ -133,6 +133,8 @@ async def async_setup_entry( class FluNearYouSensor(CoordinatorEntity, SensorEntity): """Define a base Flu Near You sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, From b4e5c95e0343dfe839fbfca7a3ed176cb28151e3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:45:09 -0600 Subject: [PATCH 321/820] Migrate OpenUV to new entity naming style (#74919) --- homeassistant/components/openuv/__init__.py | 2 ++ .../components/openuv/binary_sensor.py | 2 +- homeassistant/components/openuv/sensor.py | 20 +++++++++---------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index be003507b81..29b13ff5258 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -183,6 +183,8 @@ class OpenUV: class OpenUvEntity(Entity): """Define a generic OpenUV entity.""" + _attr_has_entity_name = True + def __init__(self, openuv: OpenUV, description: EntityDescription) -> None: """Initialize.""" self._attr_extra_state_attributes = {} diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 503d82d32f2..757f0479e01 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -18,7 +18,7 @@ ATTR_PROTECTION_WINDOW_STARTING_UV = "start_uv" BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW = BinarySensorEntityDescription( key=TYPE_PROTECTION_WINDOW, - name="Protection Window", + name="Protection window", icon="mdi:sunglasses", ) diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index f654ed63a6d..3a5bd3c2a47 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -49,68 +49,68 @@ UV_LEVEL_LOW = "Low" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_CURRENT_OZONE_LEVEL, - name="Current Ozone Level", + name="Current ozone level", device_class=SensorDeviceClass.OZONE, native_unit_of_measurement="du", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_INDEX, - name="Current UV Index", + name="Current UV index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_LEVEL, - name="Current UV Level", + name="Current UV level", icon="mdi:weather-sunny", ), SensorEntityDescription( key=TYPE_MAX_UV_INDEX, - name="Max UV Index", + name="Max UV index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_1, - name="Skin Type 1 Safe Exposure Time", + name="Skin type 1 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_2, - name="Skin Type 2 Safe Exposure Time", + name="Skin type 2 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_3, - name="Skin Type 3 Safe Exposure Time", + name="Skin type 3 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_4, - name="Skin Type 4 Safe Exposure Time", + name="Skin type 4 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_5, - name="Skin Type 5 Safe Exposure Time", + name="Skin type 5 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_6, - name="Skin Type 6 Safe Exposure Time", + name="Skin type 6 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, From c2d8335cc50d086167b391f4508299f21cd760da Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 10 Jul 2022 23:06:27 +0200 Subject: [PATCH 322/820] Add "Home Assistant (skip pip)" to VS Code launch.json (#74887) --- .vscode/launch.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index e8bf893e0c9..3cec89cc7e6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,14 @@ "justMyCode": false, "args": ["--debug", "-c", "config"] }, + { + "name": "Home Assistant (skip pip)", + "type": "python", + "request": "launch", + "module": "homeassistant", + "justMyCode": false, + "args": ["--debug", "-c", "config", "--skip-pip"] + }, { // Debug by attaching to local Home Asistant server using Remote Python Debugger. // See https://www.home-assistant.io/integrations/debugpy/ From c9330841a111a27e84f8adb7ec7103632eb0cfc0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 10 Jul 2022 23:08:33 +0200 Subject: [PATCH 323/820] Add AirQ sensors to Sensibo (#74868) --- homeassistant/components/sensibo/sensor.py | 58 +++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index fad22fdd677..8a53a0febd4 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -17,10 +17,13 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, ELECTRIC_POTENTIAL_VOLT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory @@ -106,7 +109,6 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( SensiboMotionSensorEntityDescription( key="temperature", device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, name="Temperature", icon="mdi:thermometer", @@ -143,9 +145,39 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( value_fn=lambda data: data.timer_time, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), + SensiboDeviceSensorEntityDescription( + key="feels_like", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + name="Temperature Feels Like", + value_fn=lambda data: data.feelslike, + extra_fn=None, + entity_registry_enabled_default=False, + ), FILTER_LAST_RESET_DESCRIPTION, ) +AIRQ_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( + SensiboDeviceSensorEntityDescription( + key="airq_tvoc", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:air-filter", + name="AirQ TVOC", + value_fn=lambda data: data.tvoc, + extra_fn=None, + ), + SensiboDeviceSensorEntityDescription( + key="airq_co2", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + name="AirQ CO2", + value_fn=lambda data: data.co2, + extra_fn=None, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -177,6 +209,12 @@ async def async_setup_entry( for description in DEVICE_SENSOR_TYPES if device_data.model != "pure" ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for device_id, device_data in coordinator.data.parsed.items() + for description in AIRQ_SENSOR_TYPES + if device_data.model == "airq" + ) async_add_entities(entities) @@ -207,6 +245,15 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): f"{self.device_data.name} Motion Sensor {entity_description.name}" ) + @property + def native_unit_of_measurement(self) -> str | None: + """Add native unit of measurement.""" + if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: + return ( + TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT + ) + return self.entity_description.native_unit_of_measurement + @property def native_value(self) -> StateType: """Return value of sensor.""" @@ -235,6 +282,15 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): self._attr_unique_id = f"{device_id}-{entity_description.key}" self._attr_name = f"{self.device_data.name} {entity_description.name}" + @property + def native_unit_of_measurement(self) -> str | None: + """Add native unit of measurement.""" + if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: + return ( + TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT + ) + return self.entity_description.native_unit_of_measurement + @property def native_value(self) -> StateType | datetime: """Return value of sensor.""" From 8054e309b346558e07dc3f9c729d592148432bc1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:23:40 +0200 Subject: [PATCH 324/820] Update flake8-noqa to 1.2.5 (#74896) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd2d5ac7e20..7206de9ee83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - flake8-comprehensions==3.10.0 - - flake8-noqa==1.2.1 + - flake8-noqa==1.2.5 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index c229bc51731..dd5077cc00c 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -5,7 +5,7 @@ black==22.6.0 codespell==2.1.0 flake8-comprehensions==3.10.0 flake8-docstrings==1.6.0 -flake8-noqa==1.2.1 +flake8-noqa==1.2.5 flake8==4.0.1 isort==5.10.1 mccabe==0.6.1 From 176e2754ec9752df12180ae3a102b23b037451ce Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:56:04 +0200 Subject: [PATCH 325/820] Update adb-shell to 0.4.3 (#74855) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 7bddd13c833..92d4f806b39 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.4.2", + "adb-shell[async]==0.4.3", "androidtv[async]==0.0.67", "pure-python-adb[async]==0.3.0.dev0" ], diff --git a/requirements_all.txt b/requirements_all.txt index c136010056a..62ee16e610a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -77,7 +77,7 @@ accuweather==0.3.0 adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.2 +adb-shell[async]==0.4.3 # homeassistant.components.alarmdecoder adext==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd29bcded25..e30e0d5b5c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -67,7 +67,7 @@ accuweather==0.3.0 adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.2 +adb-shell[async]==0.4.3 # homeassistant.components.alarmdecoder adext==0.4.2 From f15d3fc5dba45e4e503d7cd17e30f6f29a5329b0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:56:48 +0200 Subject: [PATCH 326/820] Migrate WLED to new entity naming style (#74860) --- homeassistant/components/wled/binary_sensor.py | 2 +- homeassistant/components/wled/button.py | 2 +- homeassistant/components/wled/light.py | 7 +++---- homeassistant/components/wled/models.py | 2 ++ homeassistant/components/wled/number.py | 7 ++----- homeassistant/components/wled/select.py | 14 ++++++-------- homeassistant/components/wled/sensor.py | 13 ++++++------- homeassistant/components/wled/switch.py | 12 ++++++------ homeassistant/components/wled/update.py | 2 +- 9 files changed, 28 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py index d2262798d50..61f8dc45f7b 100644 --- a/homeassistant/components/wled/binary_sensor.py +++ b/homeassistant/components/wled/binary_sensor.py @@ -34,6 +34,7 @@ class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.UPDATE + _attr_name = "Firmware" # Disabled by default, as this entity is deprecated. _attr_entity_registry_enabled_default = False @@ -41,7 +42,6 @@ class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Firmware" self._attr_unique_id = f"{coordinator.data.info.mac_address}_update" @property diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 97877053163..b08ee396c70 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -28,11 +28,11 @@ class WLEDRestartButton(WLEDEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Restart" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Restart" self._attr_unique_id = f"{coordinator.data.info.mac_address}_restart" @wled_exception_handler diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 4f5c758dfff..98be359628e 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -52,13 +52,13 @@ class WLEDMasterLight(WLEDEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_icon = "mdi:led-strip-variant" + _attr_name = "Master" _attr_supported_features = LightEntityFeature.TRANSITION _attr_supported_color_modes = {ColorMode.BRIGHTNESS} def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED master light.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Master" self._attr_unique_id = coordinator.data.info.mac_address @property @@ -118,9 +118,8 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - self._attr_name = f"{coordinator.data.info.name} Segment {segment}" - if segment == 0: - self._attr_name = coordinator.data.info.name + if segment != 0: + self._attr_name = f"Segment {segment}" self._attr_unique_id = ( f"{self.coordinator.data.info.mac_address}_{self._segment}" diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py index b5fc0855e04..2bdd2e46e2c 100644 --- a/homeassistant/components/wled/models.py +++ b/homeassistant/components/wled/models.py @@ -10,6 +10,8 @@ from .coordinator import WLEDDataUpdateCoordinator class WLEDEntity(CoordinatorEntity[WLEDDataUpdateCoordinator]): """Defines a base WLED entity.""" + _attr_has_entity_name = True + @property def device_info(self) -> DeviceInfo: """Return device information about this WLED device.""" diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index 6c426cc44c5..d6032791e5b 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -71,11 +71,8 @@ class WLEDNumber(WLEDEntity, NumberEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - self._attr_name = ( - f"{coordinator.data.info.name} Segment {segment} {description.name}" - ) - if segment == 0: - self._attr_name = f"{coordinator.data.info.name} {description.name}" + if segment != 0: + self._attr_name = f"Segment {segment} {description.name}" self._attr_unique_id = ( f"{coordinator.data.info.mac_address}_{description.key}_{segment}" diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index e555b3422ce..c3980f9f9c7 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -51,12 +51,12 @@ class WLEDLiveOverrideSelect(WLEDEntity, SelectEntity): _attr_device_class = DEVICE_CLASS_WLED_LIVE_OVERRIDE _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:theater" + _attr_name = "Live override" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED .""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Live Override" self._attr_unique_id = f"{coordinator.data.info.mac_address}_live_override" self._attr_options = [str(live.value) for live in Live] @@ -75,12 +75,12 @@ class WLEDPresetSelect(WLEDEntity, SelectEntity): """Defined a WLED Preset select.""" _attr_icon = "mdi:playlist-play" + _attr_name = "Preset" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED .""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Preset" self._attr_unique_id = f"{coordinator.data.info.mac_address}_preset" self._attr_options = [preset.name for preset in self.coordinator.data.presets] @@ -106,12 +106,12 @@ class WLEDPlaylistSelect(WLEDEntity, SelectEntity): """Define a WLED Playlist select.""" _attr_icon = "mdi:play-speed" + _attr_name = "Playlist" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED playlist.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Playlist" self._attr_unique_id = f"{coordinator.data.info.mac_address}_playlist" self._attr_options = [ playlist.name for playlist in self.coordinator.data.playlists @@ -140,6 +140,7 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity): _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:palette-outline" + _attr_name = "Color palette" _segment: int def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: @@ -148,11 +149,8 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - self._attr_name = ( - f"{coordinator.data.info.name} Segment {segment} Color Palette" - ) - if segment == 0: - self._attr_name = f"{coordinator.data.info.name} Color Palette" + if segment != 0: + self._attr_name = f"Segment {segment} color palette" self._attr_unique_id = f"{coordinator.data.info.mac_address}_palette_{segment}" self._attr_options = [ diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 720158938c7..4a677910273 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -50,7 +50,7 @@ class WLEDSensorEntityDescription( SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( WLEDSensorEntityDescription( key="estimated_current", - name="Estimated Current", + name="Estimated current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -60,13 +60,13 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="info_leds_count", - name="LED Count", + name="LED count", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.leds.count, ), WLEDSensorEntityDescription( key="info_leds_max_power", - name="Max Current", + name="Max current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.CURRENT, @@ -83,7 +83,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="free_heap", - name="Free Memory", + name="Free memory", icon="mdi:memory", native_unit_of_measurement=DATA_BYTES, state_class=SensorStateClass.MEASUREMENT, @@ -93,7 +93,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="wifi_signal", - name="Wi-Fi Signal", + name="Wi-Fi signal", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -111,7 +111,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="wifi_channel", - name="Wi-Fi Channel", + name="Wi-Fi channel", icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -155,7 +155,6 @@ class WLEDSensorEntity(WLEDEntity, SensorEntity): """Initialize a WLED sensor entity.""" super().__init__(coordinator=coordinator) self.entity_description = description - self._attr_name = f"{coordinator.data.info.name} {description.name}" self._attr_unique_id = f"{coordinator.data.info.mac_address}_{description.key}" @property diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index e98b3494ad6..7d0d9ee24fb 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -55,11 +55,11 @@ class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:weather-night" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Nightlight" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED nightlight switch.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Nightlight" self._attr_unique_id = f"{coordinator.data.info.mac_address}_nightlight" @property @@ -92,11 +92,11 @@ class WLEDSyncSendSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:upload-network-outline" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Sync send" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync send switch.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Sync Send" self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_send" @property @@ -125,11 +125,11 @@ class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:download-network-outline" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Sync receive" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync receive switch.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Sync Receive" self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_receive" @property @@ -158,6 +158,7 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:swap-horizontal-bold" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Reverse" _segment: int def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: @@ -166,9 +167,8 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - self._attr_name = f"{coordinator.data.info.name} Segment {segment} Reverse" - if segment == 0: - self._attr_name = f"{coordinator.data.info.name} Reverse" + if segment != 0: + self._attr_name = f"Segment {segment} reverse" self._attr_unique_id = f"{coordinator.data.info.mac_address}_reverse_{segment}" self._segment = segment diff --git a/homeassistant/components/wled/update.py b/homeassistant/components/wled/update.py index f0fc532b3b3..75546fdac1a 100644 --- a/homeassistant/components/wled/update.py +++ b/homeassistant/components/wled/update.py @@ -36,11 +36,11 @@ class WLEDUpdateEntity(WLEDEntity, UpdateEntity): UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION ) _attr_title = "WLED" + _attr_name = "Firmware" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the update entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Firmware" self._attr_unique_id = coordinator.data.info.mac_address @property From 08887a6faa19cc56d1c13a536f236b0ae404c202 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:57:46 +0200 Subject: [PATCH 327/820] Update yamllint to 1.27.1 (#74853) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7206de9ee83..91bda72e616 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,7 @@ repos: - --branch=master - --branch=rc - repo: https://github.com/adrienverge/yamllint.git - rev: v1.26.3 + rev: v1.27.1 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index dd5077cc00c..caed3668db7 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -13,4 +13,4 @@ pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 pyupgrade==2.34.0 -yamllint==1.26.3 +yamllint==1.27.1 From 792c825699f0c03569bd0da01c88efdfed9d6446 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:58:05 +0200 Subject: [PATCH 328/820] Update numpy to 1.23.1 (#74851) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 509d5740b22..5b1ff9715d1 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.23.0"], + "requirements": ["numpy==1.23.1"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 7485ff9d608..561ebdb6e89 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.23.0", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.23.1", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 0272feb0f9e..cada1199084 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.23.0", "opencv-python-headless==4.6.0.66"], + "requirements": ["numpy==1.23.1", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index dd88fd7e277..c8a62791429 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.23.0", + "numpy==1.23.1", "pillow==9.2.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index b579cc036bb..e70056f207a 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.23.0"], + "requirements": ["numpy==1.23.1"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d53db9a7464..d443a1f66aa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -88,7 +88,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.0 +numpy==1.23.1 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 62ee16e610a..c78dee5d185 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,7 +1129,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.0 +numpy==1.23.1 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e30e0d5b5c3..f82b2d1a25e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -785,7 +785,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.0 +numpy==1.23.1 # homeassistant.components.google oauth2client==4.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 11e88976e83..10951bcc7e8 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,7 +106,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.0 +numpy==1.23.1 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 From a4517a4c1d9dc9149b7fc4a916b7950804e9881f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:59:08 +0200 Subject: [PATCH 329/820] Trigger full CI on Bluetooth integration changes (#74929) --- .core_files.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.core_files.yaml b/.core_files.yaml index 55b543a333e..29390865611 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -52,6 +52,7 @@ components: &components - homeassistant/components/auth/** - homeassistant/components/automation/** - homeassistant/components/backup/** + - homeassistant/components/bluetooth/** - homeassistant/components/cloud/** - homeassistant/components/config/** - homeassistant/components/configurator/** From 9ff77e0fa1af036b4b0abf8c56468b7760d83e74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 00:06:45 +0200 Subject: [PATCH 330/820] Update pytest-sugar is 0.9.5 (#74931) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 822fb02c6e0..dbb7a76dac4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -20,7 +20,7 @@ pytest-cov==3.0.0 pytest-freezegun==0.4.2 pytest-socket==0.5.1 pytest-test-groups==1.0.3 -pytest-sugar==0.9.4 +pytest-sugar==0.9.5 pytest-timeout==2.1.0 pytest-xdist==2.5.0 pytest==7.1.2 From 261c52e26087189e45ff1eac2a70da8919b6eaef Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Mon, 11 Jul 2022 00:10:55 +0200 Subject: [PATCH 331/820] Alexa: Fix duplicate proactive reports (#74930) --- homeassistant/components/alexa/config.py | 4 ++-- homeassistant/components/alexa/smart_home_http.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index b6cbe6ba74b..9f51d92a229 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -65,6 +65,7 @@ class AbstractConfig(ABC): async def async_enable_proactive_mode(self): """Enable proactive mode.""" + _LOGGER.debug("Enable proactive mode") if self._unsub_proactive_report is None: self._unsub_proactive_report = self.hass.async_create_task( async_enable_proactive_mode(self.hass, self) @@ -77,6 +78,7 @@ class AbstractConfig(ABC): async def async_disable_proactive_mode(self): """Disable proactive mode.""" + _LOGGER.debug("Disable proactive mode") if unsub_func := await self._unsub_proactive_report: unsub_func() self._unsub_proactive_report = None @@ -113,7 +115,6 @@ class AbstractConfig(ABC): self._store.set_authorized(authorized) if self.should_report_state != self.is_reporting_states: if self.should_report_state: - _LOGGER.debug("Enable proactive mode") try: await self.async_enable_proactive_mode() except Exception: @@ -121,7 +122,6 @@ class AbstractConfig(ABC): self._store.set_authorized(False) raise else: - _LOGGER.debug("Disable proactive mode") await self.async_disable_proactive_mode() diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 6a953a9f9d4..9be7381adb6 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -12,7 +12,6 @@ from .auth import Auth from .config import AbstractConfig from .const import CONF_ENDPOINT, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE from .smart_home import async_handle_message -from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" @@ -104,7 +103,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: hass.http.register_view(SmartHomeView(smart_home_config)) if smart_home_config.should_report_state: - await async_enable_proactive_mode(hass, smart_home_config) + await smart_home_config.async_enable_proactive_mode() class SmartHomeView(HomeAssistantView): From d697bb53c5a32ebd4b58b4298f63d894fe9d4aaa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 00:11:43 +0200 Subject: [PATCH 332/820] Update lru-dict to 1.1.8 (#74932) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d443a1f66aa..c92bf170f45 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ home-assistant-frontend==20220707.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 -lru-dict==1.1.7 +lru-dict==1.1.8 orjson==3.7.7 paho-mqtt==1.6.1 pillow==9.2.0 diff --git a/pyproject.toml b/pyproject.toml index 621bbf68999..41059773977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "httpx==0.23.0", "ifaddr==0.1.7", "jinja2==3.1.2", - "lru-dict==1.1.7", + "lru-dict==1.1.8", "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", diff --git a/requirements.txt b/requirements.txt index fefa0f33ecb..84d8711753e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ ciso8601==2.2.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 -lru-dict==1.1.7 +lru-dict==1.1.8 PyJWT==2.4.0 cryptography==36.0.2 orjson==3.7.7 From abb11009b4ce1455434d54f6d44488da506e40bb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 11 Jul 2022 00:23:28 +0000 Subject: [PATCH 333/820] [ci skip] Translation update --- .../here_travel_time/translations/ja.json | 7 +++++++ .../here_travel_time/translations/pt.json | 13 +++++++++++++ .../season/translations/sensor.pt.json | 6 ++++++ .../zodiac/translations/sensor.pt.json | 18 ++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 homeassistant/components/here_travel_time/translations/pt.json create mode 100644 homeassistant/components/zodiac/translations/sensor.pt.json diff --git a/homeassistant/components/here_travel_time/translations/ja.json b/homeassistant/components/here_travel_time/translations/ja.json index 6db262d829a..ae3eb1d4f73 100644 --- a/homeassistant/components/here_travel_time/translations/ja.json +++ b/homeassistant/components/here_travel_time/translations/ja.json @@ -39,6 +39,13 @@ }, "title": "\u539f\u70b9(Origin)\u3092\u9078\u629e" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u5730\u56f3\u4e0a\u306e\u5834\u6240\u3092\u4f7f\u7528\u3059\u308b", + "origin_entity": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4f7f\u7528\u3059\u308b" + }, + "title": "\u539f\u70b9(Origin)\u3092\u9078\u629e" + }, "user": { "data": { "api_key": "API\u30ad\u30fc", diff --git a/homeassistant/components/here_travel_time/translations/pt.json b/homeassistant/components/here_travel_time/translations/pt.json new file mode 100644 index 00000000000..f412c0033f6 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "origin_menu": { + "menu_options": { + "origin_coordinates": "Usando uma localiza\u00e7\u00e3o no mapa", + "origin_entity": "Usando uma entidade" + }, + "title": "Escolha a Origem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.pt.json b/homeassistant/components/season/translations/sensor.pt.json index 4c81e432350..e30461da7d2 100644 --- a/homeassistant/components/season/translations/sensor.pt.json +++ b/homeassistant/components/season/translations/sensor.pt.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "Outono ", + "spring": "Primavera", + "summer": "Ver\u00e3o ", + "winter": "Inverno" + }, "season__season__": { "autumn": "Outono", "spring": "Primavera", diff --git a/homeassistant/components/zodiac/translations/sensor.pt.json b/homeassistant/components/zodiac/translations/sensor.pt.json new file mode 100644 index 00000000000..b37a6886df0 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.pt.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Aqu\u00e1rio ", + "aries": "Carneiro ", + "cancer": "Caranguejo ", + "capricorn": "Capric\u00f3rnio", + "gemini": "G\u00e9meos ", + "leo": "Le\u00e3o ", + "libra": "Balan\u00e7a", + "pisces": "Peixes", + "sagittarius": "Sagit\u00e1rio", + "scorpio": "Escorpi\u00e3o ", + "taurus": "Touro", + "virgo": "Virgem" + } + } +} \ No newline at end of file From 7a729aed54c4f61be993097a758a57b8878acbb4 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 11 Jul 2022 14:03:48 +1000 Subject: [PATCH 334/820] Migrate Advantage Air to new entity naming style (#74940) --- .../components/advantage_air/binary_sensor.py | 10 +++++----- homeassistant/components/advantage_air/cover.py | 2 +- homeassistant/components/advantage_air/entity.py | 2 ++ homeassistant/components/advantage_air/select.py | 2 +- homeassistant/components/advantage_air/sensor.py | 8 ++++---- .../components/advantage_air/test_binary_sensor.py | 12 ++++++------ tests/components/advantage_air/test_climate.py | 6 +++--- tests/components/advantage_air/test_cover.py | 10 +++++----- tests/components/advantage_air/test_select.py | 2 +- tests/components/advantage_air/test_sensor.py | 14 +++++++------- tests/components/advantage_air/test_switch.py | 2 +- 11 files changed, 36 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 73b10b158b0..bc302e5d4a6 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry( entities: list[BinarySensorEntity] = [] for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - entities.append(AdvantageAirZoneFilter(instance, ac_key)) + entities.append(AdvantageAirFilter(instance, ac_key)) for zone_key, zone in ac_device["zones"].items(): # Only add motion sensor when motion is enabled if zone["motionConfig"] >= 2: @@ -38,7 +38,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): +class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Filter.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM @@ -47,7 +47,7 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air Filter.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} Filter' + self._attr_name = f'{self._ac["name"]} filter' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-filter' ) @@ -66,7 +66,7 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Motion.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} Motion' + self._attr_name = f'{self._zone["name"]} motion' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-motion' ) @@ -86,7 +86,7 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone MyZone.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} MyZone' + self._attr_name = f'{self._zone["name"]} myZone' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-myzone' ) diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 36ae2c7fff0..f8240126476 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -52,7 +52,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Cover Class.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]}' + self._attr_name = self._zone["name"] self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}' ) diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 9514cc7915b..b0ff1bfb8c8 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -9,6 +9,8 @@ from .const import DOMAIN class AdvantageAirEntity(CoordinatorEntity): """Parent class for Advantage Air Entities.""" + _attr_has_entity_name = True + def __init__(self, instance, ac_key, zone_key=None): """Initialize common aspects of an Advantage Air sensor.""" super().__init__(instance["coordinator"]) diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index ecc612ae1ed..ff2a555bc80 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -36,7 +36,7 @@ class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} MyZone' + self._attr_name = f'{self._ac["name"]} myZone' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-myzone' ) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 8055c37a571..855a0e6d15f 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -67,7 +67,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): super().__init__(instance, ac_key) self.action = action self._time_key = f"countDownTo{action}" - self._attr_name = f'{self._ac["name"]} Time To {action}' + self._attr_name = f'{self._ac["name"]} time to {action}' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}' ) @@ -100,7 +100,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Vent Sensor.""" super().__init__(instance, ac_key, zone_key=zone_key) - self._attr_name = f'{self._zone["name"]} Vent' + self._attr_name = f'{self._zone["name"]} vent' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-vent' ) @@ -130,7 +130,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone wireless signal sensor.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} Signal' + self._attr_name = f'{self._zone["name"]} signal' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal' ) @@ -166,7 +166,7 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Temp Sensor.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} Temperature' + self._attr_name = f'{self._zone["name"]} temperature' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-temp' ) diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 275b5fc4e52..9aa092a8679 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -34,7 +34,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test First Air Filter - entity_id = "binary_sensor.ac_one_filter" + entity_id = "binary_sensor.testname_ac_one_filter" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -44,7 +44,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-filter" # Test Second Air Filter - entity_id = "binary_sensor.ac_two_filter" + entity_id = "binary_sensor.testname_ac_two_filter" state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -54,7 +54,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac2-filter" # Test First Motion Sensor - entity_id = "binary_sensor.zone_open_with_sensor_motion" + entity_id = "binary_sensor.testname_zone_open_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -64,7 +64,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-motion" # Test Second Motion Sensor - entity_id = "binary_sensor.zone_closed_with_sensor_motion" + entity_id = "binary_sensor.testname_zone_closed_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -74,7 +74,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-motion" # Test First MyZone Sensor (disabled by default) - entity_id = "binary_sensor.zone_open_with_sensor_myzone" + entity_id = "binary_sensor.testname_zone_open_with_sensor_myzone" assert not hass.states.get(entity_id) @@ -96,7 +96,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-myzone" # Test Second Motion Sensor (disabled by default) - entity_id = "binary_sensor.zone_closed_with_sensor_myzone" + entity_id = "binary_sensor.testname_zone_closed_with_sensor_myzone" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 4d075a36151..27f89d1df3e 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -51,7 +51,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Main Climate Entity - entity_id = "climate.ac_one" + entity_id = "climate.testname_ac_one" state = hass.states.get(entity_id) assert state assert state.state == HVACMode.FAN_ONLY @@ -122,7 +122,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test Climate Zone Entity - entity_id = "climate.zone_open_with_sensor" + entity_id = "climate.testname_zone_open_with_sensor" state = hass.states.get(entity_id) assert state assert state.attributes.get("min_temp") == 16 @@ -204,7 +204,7 @@ async def test_climate_async_failed_update(hass, aioclient_mock): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: ["climate.ac_one"], ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: ["climate.testname_ac_one"], ATTR_TEMPERATURE: 25}, blocking=True, ) assert len(aioclient_mock.mock_calls) == 2 diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index 2868179b3ee..fa3abda5138 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -45,7 +45,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Cover Zone Entity - entity_id = "cover.zone_open_without_sensor" + entity_id = "cover.testname_zone_open_without_sensor" state = hass.states.get(entity_id) assert state assert state.state == STATE_OPEN @@ -119,8 +119,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_CLOSE_COVER, { ATTR_ENTITY_ID: [ - "cover.zone_open_without_sensor", - "cover.zone_closed_without_sensor", + "cover.testname_zone_open_without_sensor", + "cover.testname_zone_closed_without_sensor", ] }, blocking=True, @@ -134,8 +134,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_OPEN_COVER, { ATTR_ENTITY_ID: [ - "cover.zone_open_without_sensor", - "cover.zone_closed_without_sensor", + "cover.testname_zone_open_without_sensor", + "cover.testname_zone_closed_without_sensor", ] }, blocking=True, diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index 3d246f566e5..41ed9c407fc 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -37,7 +37,7 @@ async def test_select_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Select Entity - entity_id = "select.ac_one_myzone" + entity_id = "select.testname_ac_one_myzone" state = hass.states.get(entity_id) assert state assert state.state == "Zone open with Sensor" diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 997f11dea91..70322f4c9df 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test First TimeToOn Sensor - entity_id = "sensor.ac_one_time_to_on" + entity_id = "sensor.testname_ac_one_time_to_on" state = hass.states.get(entity_id) assert state assert int(state.state) == 0 @@ -66,7 +66,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test First TimeToOff Sensor - entity_id = "sensor.ac_one_time_to_off" + entity_id = "sensor.testname_ac_one_time_to_off" state = hass.states.get(entity_id) assert state assert int(state.state) == 10 @@ -91,7 +91,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test First Zone Vent Sensor - entity_id = "sensor.zone_open_with_sensor_vent" + entity_id = "sensor.testname_zone_open_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 100 @@ -101,7 +101,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-vent" # Test Second Zone Vent Sensor - entity_id = "sensor.zone_closed_with_sensor_vent" + entity_id = "sensor.testname_zone_closed_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 0 @@ -111,7 +111,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-vent" # Test First Zone Signal Sensor - entity_id = "sensor.zone_open_with_sensor_signal" + entity_id = "sensor.testname_zone_open_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 40 @@ -121,7 +121,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-signal" # Test Second Zone Signal Sensor - entity_id = "sensor.zone_closed_with_sensor_signal" + entity_id = "sensor.testname_zone_closed_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 10 @@ -131,7 +131,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-signal" # Test First Zone Temp Sensor (disabled by default) - entity_id = "sensor.zone_open_with_sensor_temperature" + entity_id = "sensor.testname_zone_open_with_sensor_temperature" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py index 1a78025df70..1d99d7f29c1 100644 --- a/tests/components/advantage_air/test_switch.py +++ b/tests/components/advantage_air/test_switch.py @@ -41,7 +41,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Switch Entity - entity_id = "switch.ac_one_fresh_air" + entity_id = "switch.testname_ac_one_fresh_air" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF From d0f71d2e53ce98ec35757d742b438661f888efa1 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 11 Jul 2022 14:04:18 +1000 Subject: [PATCH 335/820] Migrate Aussie Broadband to new entity naming style (#74937) --- .../components/aussie_broadband/sensor.py | 22 +++++----- .../aussie_broadband/test_sensor.py | 44 +++++++++++++------ 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py index 09946cef03d..ecc891deba9 100644 --- a/homeassistant/components/aussie_broadband/sensor.py +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -33,7 +33,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Internet Services sensors SensorValueEntityDescription( key="usedMb", - name="Data Used", + name="Data used", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:network", @@ -55,35 +55,35 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Mobile Phone Services sensors SensorValueEntityDescription( key="national", - name="National Calls", + name="National calls", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="mobile", - name="Mobile Calls", + name="Mobile calls", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="international", - name="International Calls", + name="International calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone-plus", ), SensorValueEntityDescription( key="sms", - name="SMS Sent", + name="SMS sent", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:message-processing", value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="internet", - name="Data Used", + name="Data used", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_KILOBYTES, icon="mdi:network", @@ -91,14 +91,14 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( ), SensorValueEntityDescription( key="voicemail", - name="Voicemail Calls", + name="Voicemail calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", ), SensorValueEntityDescription( key="other", - name="Other Calls", + name="Other calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", @@ -106,13 +106,13 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Generic sensors SensorValueEntityDescription( key="daysTotal", - name="Billing Cycle Length", + name="Billing cycle length", native_unit_of_measurement=TIME_DAYS, icon="mdi:calendar-range", ), SensorValueEntityDescription( key="daysRemaining", - name="Billing Cycle Remaining", + name="Billing cycle remaining", native_unit_of_measurement=TIME_DAYS, icon="mdi:calendar-clock", ), @@ -137,6 +137,7 @@ async def async_setup_entry( class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): """Base class for Aussie Broadband metric sensors.""" + _attr_has_entity_name = True entity_description: SensorValueEntityDescription def __init__( @@ -146,7 +147,6 @@ class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): super().__init__(service["coordinator"]) self.entity_description = description self._attr_unique_id = f"{service[SERVICE_ID]}:{description.key}" - self._attr_name = f"{service['name']} {description.name}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, service[SERVICE_ID])}, diff --git a/tests/components/aussie_broadband/test_sensor.py b/tests/components/aussie_broadband/test_sensor.py index c99c52d5c86..2db4b79dbe9 100644 --- a/tests/components/aussie_broadband/test_sensor.py +++ b/tests/components/aussie_broadband/test_sensor.py @@ -44,11 +44,17 @@ async def test_nbn_sensor_states(hass): await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_NBN_USAGE) - assert hass.states.get("sensor.nbn_data_used").state == "54321" - assert hass.states.get("sensor.nbn_downloaded").state == "50000" - assert hass.states.get("sensor.nbn_uploaded").state == "4321" - assert hass.states.get("sensor.nbn_billing_cycle_length").state == "28" - assert hass.states.get("sensor.nbn_billing_cycle_remaining").state == "25" + assert hass.states.get("sensor.fake_abb_nbn_service_data_used").state == "54321" + assert hass.states.get("sensor.fake_abb_nbn_service_downloaded").state == "50000" + assert hass.states.get("sensor.fake_abb_nbn_service_uploaded").state == "4321" + assert ( + hass.states.get("sensor.fake_abb_nbn_service_billing_cycle_length").state + == "28" + ) + assert ( + hass.states.get("sensor.fake_abb_nbn_service_billing_cycle_remaining").state + == "25" + ) async def test_phone_sensor_states(hass): @@ -56,12 +62,18 @@ async def test_phone_sensor_states(hass): await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_MOBILE_USAGE) - assert hass.states.get("sensor.mobile_national_calls").state == "1" - assert hass.states.get("sensor.mobile_mobile_calls").state == "2" - assert hass.states.get("sensor.mobile_sms_sent").state == "4" - assert hass.states.get("sensor.mobile_data_used").state == "512" - assert hass.states.get("sensor.mobile_billing_cycle_length").state == "31" - assert hass.states.get("sensor.mobile_billing_cycle_remaining").state == "30" + assert hass.states.get("sensor.fake_abb_mobile_service_national_calls").state == "1" + assert hass.states.get("sensor.fake_abb_mobile_service_mobile_calls").state == "2" + assert hass.states.get("sensor.fake_abb_mobile_service_sms_sent").state == "4" + assert hass.states.get("sensor.fake_abb_mobile_service_data_used").state == "512" + assert ( + hass.states.get("sensor.fake_abb_mobile_service_billing_cycle_length").state + == "31" + ) + assert ( + hass.states.get("sensor.fake_abb_mobile_service_billing_cycle_remaining").state + == "30" + ) async def test_voip_sensor_states(hass): @@ -69,6 +81,10 @@ async def test_voip_sensor_states(hass): await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_VOIP_USAGE) - assert hass.states.get("sensor.mobile_national_calls").state == "1" - assert hass.states.get("sensor.mobile_sms_sent").state == STATE_UNKNOWN - assert hass.states.get("sensor.mobile_data_used").state == STATE_UNKNOWN + assert hass.states.get("sensor.fake_abb_voip_service_national_calls").state == "1" + assert ( + hass.states.get("sensor.fake_abb_voip_service_sms_sent").state == STATE_UNKNOWN + ) + assert ( + hass.states.get("sensor.fake_abb_voip_service_data_used").state == STATE_UNKNOWN + ) From 53502eb6625eb167ca41247b42978359e375e04c Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Mon, 11 Jul 2022 00:04:54 -0400 Subject: [PATCH 336/820] Migrate Mazda to new entity naming style (#74939) --- homeassistant/components/mazda/__init__.py | 2 ++ .../components/mazda/binary_sensor.py | 18 ++++++--------- homeassistant/components/mazda/button.py | 23 +++++-------------- .../components/mazda/device_tracker.py | 2 +- homeassistant/components/mazda/lock.py | 3 ++- homeassistant/components/mazda/sensor.py | 22 ++++++++---------- homeassistant/components/mazda/switch.py | 2 +- tests/components/mazda/test_binary_sensor.py | 10 ++++---- tests/components/mazda/test_button.py | 18 +++++++-------- tests/components/mazda/test_device_tracker.py | 2 +- tests/components/mazda/test_sensor.py | 16 ++++++------- 11 files changed, 51 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index f5685155587..b62725e5b1b 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -222,6 +222,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class MazdaEntity(CoordinatorEntity): """Defines a base Mazda entity.""" + _attr_has_entity_name = True + def __init__(self, client, coordinator, index): """Initialize the Mazda entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/mazda/binary_sensor.py b/homeassistant/components/mazda/binary_sensor.py index cc60d6318c7..c2727654525 100644 --- a/homeassistant/components/mazda/binary_sensor.py +++ b/homeassistant/components/mazda/binary_sensor.py @@ -22,9 +22,6 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN class MazdaBinarySensorRequiredKeysMixin: """Mixin for required keys.""" - # Suffix to be appended to the vehicle name to obtain the binary sensor name - name_suffix: str - # Function to determine the value for this binary sensor, given the coordinator data value_fn: Callable[[dict[str, Any]], bool] @@ -49,49 +46,49 @@ def _plugged_in_supported(data): BINARY_SENSOR_ENTITIES = [ MazdaBinarySensorEntityDescription( key="driver_door", - name_suffix="Driver Door", + name="Driver door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"], ), MazdaBinarySensorEntityDescription( key="passenger_door", - name_suffix="Passenger Door", + name="Passenger door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"], ), MazdaBinarySensorEntityDescription( key="rear_left_door", - name_suffix="Rear Left Door", + name="Rear left door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"], ), MazdaBinarySensorEntityDescription( key="rear_right_door", - name_suffix="Rear Right Door", + name="Rear right door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"], ), MazdaBinarySensorEntityDescription( key="trunk", - name_suffix="Trunk", + name="Trunk", icon="mdi:car-back", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["trunkOpen"], ), MazdaBinarySensorEntityDescription( key="hood", - name_suffix="Hood", + name="Hood", icon="mdi:car", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["hoodOpen"], ), MazdaBinarySensorEntityDescription( key="ev_plugged_in", - name_suffix="Plugged In", + name="Plugged in", device_class=BinarySensorDeviceClass.PLUG, is_supported=_plugged_in_supported, value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"], @@ -126,7 +123,6 @@ class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity): super().__init__(client, coordinator, index) self.entity_description = description - self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" @property diff --git a/homeassistant/components/mazda/button.py b/homeassistant/components/mazda/button.py index e747cb33dc2..d9b21df5156 100644 --- a/homeassistant/components/mazda/button.py +++ b/homeassistant/components/mazda/button.py @@ -61,17 +61,7 @@ async def handle_refresh_vehicle_status( @dataclass -class MazdaButtonRequiredKeysMixin: - """Mixin for required keys.""" - - # Suffix to be appended to the vehicle name to obtain the button name - name_suffix: str - - -@dataclass -class MazdaButtonEntityDescription( - ButtonEntityDescription, MazdaButtonRequiredKeysMixin -): +class MazdaButtonEntityDescription(ButtonEntityDescription): """Describes a Mazda button entity.""" # Function to determine whether the vehicle supports this button, given the coordinator data @@ -85,27 +75,27 @@ class MazdaButtonEntityDescription( BUTTON_ENTITIES = [ MazdaButtonEntityDescription( key="start_engine", - name_suffix="Start Engine", + name="Start engine", icon="mdi:engine", ), MazdaButtonEntityDescription( key="stop_engine", - name_suffix="Stop Engine", + name="Stop engine", icon="mdi:engine-off", ), MazdaButtonEntityDescription( key="turn_on_hazard_lights", - name_suffix="Turn On Hazard Lights", + name="Turn on hazard lights", icon="mdi:hazard-lights", ), MazdaButtonEntityDescription( key="turn_off_hazard_lights", - name_suffix="Turn Off Hazard Lights", + name="Turn off hazard lights", icon="mdi:hazard-lights", ), MazdaButtonEntityDescription( key="refresh_vehicle_status", - name_suffix="Refresh Status", + name="Refresh status", icon="mdi:refresh", async_press=handle_refresh_vehicle_status, is_supported=lambda data: data["isElectric"], @@ -146,7 +136,6 @@ class MazdaButtonEntity(MazdaEntity, ButtonEntity): super().__init__(client, coordinator, index) self.entity_description = description - self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" async def async_press(self) -> None: diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py index 13266cd64d7..79a9ef90b9b 100644 --- a/homeassistant/components/mazda/device_tracker.py +++ b/homeassistant/components/mazda/device_tracker.py @@ -29,6 +29,7 @@ async def async_setup_entry( class MazdaDeviceTracker(MazdaEntity, TrackerEntity): """Class for the device tracker.""" + _attr_name = "Device tracker" _attr_icon = "mdi:car" _attr_force_update = False @@ -36,7 +37,6 @@ class MazdaDeviceTracker(MazdaEntity, TrackerEntity): """Initialize Mazda device tracker.""" super().__init__(client, coordinator, index) - self._attr_name = f"{self.vehicle_name} Device Tracker" self._attr_unique_id = self.vin @property diff --git a/homeassistant/components/mazda/lock.py b/homeassistant/components/mazda/lock.py index bcd409d2faf..1f42c5dce48 100644 --- a/homeassistant/components/mazda/lock.py +++ b/homeassistant/components/mazda/lock.py @@ -32,11 +32,12 @@ async def async_setup_entry( class MazdaLock(MazdaEntity, LockEntity): """Class for the lock.""" + _attr_name = "Lock" + def __init__(self, client, coordinator, index) -> None: """Initialize Mazda lock.""" super().__init__(client, coordinator, index) - self._attr_name = f"{self.vehicle_name} Lock" self._attr_unique_id = self.vin @property diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 7e8b45e0ca1..c688ac62637 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -32,9 +32,6 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN class MazdaSensorRequiredKeysMixin: """Mixin for required keys.""" - # Suffix to be appended to the vehicle name to obtain the sensor name - name_suffix: str - # Function to determine the value for this sensor, given the coordinator data and the configured unit system value: Callable[[dict[str, Any], UnitSystem], StateType] @@ -159,7 +156,7 @@ def _ev_remaining_range_value(data, unit_system): SENSOR_ENTITIES = [ MazdaSensorEntityDescription( key="fuel_remaining_percentage", - name_suffix="Fuel Remaining Percentage", + name="Fuel remaining percentage", icon="mdi:gas-station", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -168,7 +165,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="fuel_distance_remaining", - name_suffix="Fuel Distance Remaining", + name="Fuel distance remaining", icon="mdi:gas-station", unit=_get_distance_unit, state_class=SensorStateClass.MEASUREMENT, @@ -177,7 +174,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="odometer", - name_suffix="Odometer", + name="Odometer", icon="mdi:speedometer", unit=_get_distance_unit, state_class=SensorStateClass.TOTAL_INCREASING, @@ -186,7 +183,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="front_left_tire_pressure", - name_suffix="Front Left Tire Pressure", + name="Front left tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -196,7 +193,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="front_right_tire_pressure", - name_suffix="Front Right Tire Pressure", + name="Front right tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -206,7 +203,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="rear_left_tire_pressure", - name_suffix="Rear Left Tire Pressure", + name="Rear left tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -216,7 +213,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="rear_right_tire_pressure", - name_suffix="Rear Right Tire Pressure", + name="Rear right tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -226,7 +223,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="ev_charge_level", - name_suffix="Charge Level", + name="Charge level", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -235,7 +232,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="ev_remaining_range", - name_suffix="Remaining Range", + name="Remaining range", icon="mdi:ev-station", unit=_get_distance_unit, state_class=SensorStateClass.MEASUREMENT, @@ -276,7 +273,6 @@ class MazdaSensorEntity(MazdaEntity, SensorEntity): super().__init__(client, coordinator, index) self.entity_description = description - self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" @property diff --git a/homeassistant/components/mazda/switch.py b/homeassistant/components/mazda/switch.py index 3ab3028425f..844585720d7 100644 --- a/homeassistant/components/mazda/switch.py +++ b/homeassistant/components/mazda/switch.py @@ -30,6 +30,7 @@ async def async_setup_entry( class MazdaChargingSwitch(MazdaEntity, SwitchEntity): """Class for the charging switch.""" + _attr_name = "Charging" _attr_icon = "mdi:ev-station" def __init__( @@ -41,7 +42,6 @@ class MazdaChargingSwitch(MazdaEntity, SwitchEntity): """Initialize Mazda charging switch.""" super().__init__(client, coordinator, index) - self._attr_name = f"{self.vehicle_name} Charging" self._attr_unique_id = self.vin @property diff --git a/tests/components/mazda/test_binary_sensor.py b/tests/components/mazda/test_binary_sensor.py index f2b272109c9..aa7cc306708 100644 --- a/tests/components/mazda/test_binary_sensor.py +++ b/tests/components/mazda/test_binary_sensor.py @@ -16,7 +16,7 @@ async def test_binary_sensors(hass): # Driver Door state = hass.states.get("binary_sensor.my_mazda3_driver_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "off" @@ -27,7 +27,7 @@ async def test_binary_sensors(hass): # Passenger Door state = hass.states.get("binary_sensor.my_mazda3_passenger_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "on" @@ -38,7 +38,7 @@ async def test_binary_sensors(hass): # Rear Left Door state = hass.states.get("binary_sensor.my_mazda3_rear_left_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "off" @@ -49,7 +49,7 @@ async def test_binary_sensors(hass): # Rear Right Door state = hass.states.get("binary_sensor.my_mazda3_rear_right_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "off" @@ -90,7 +90,7 @@ async def test_electric_vehicle_binary_sensors(hass): # Plugged In state = hass.states.get("binary_sensor.my_mazda3_plugged_in") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged In" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged in" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG assert state.state == "on" entry = entity_registry.async_get("binary_sensor.my_mazda3_plugged_in") diff --git a/tests/components/mazda/test_button.py b/tests/components/mazda/test_button.py index 71b16434ee3..81ad175020e 100644 --- a/tests/components/mazda/test_button.py +++ b/tests/components/mazda/test_button.py @@ -22,7 +22,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_start_engine" state = hass.states.get("button.my_mazda3_start_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine" entry = entity_registry.async_get("button.my_mazda3_stop_engine") @@ -30,7 +30,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_stop_engine" state = hass.states.get("button.my_mazda3_stop_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") @@ -38,7 +38,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights" assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") @@ -47,7 +47,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights" ) assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" @@ -69,7 +69,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_start_engine" state = hass.states.get("button.my_mazda3_start_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine" entry = entity_registry.async_get("button.my_mazda3_stop_engine") @@ -77,7 +77,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_stop_engine" state = hass.states.get("button.my_mazda3_stop_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") @@ -85,7 +85,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights" assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") @@ -94,7 +94,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights" ) assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" @@ -103,7 +103,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_refresh_vehicle_status" state = hass.states.get("button.my_mazda3_refresh_status") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh Status" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh status" assert state.attributes.get(ATTR_ICON) == "mdi:refresh" diff --git a/tests/components/mazda/test_device_tracker.py b/tests/components/mazda/test_device_tracker.py index 4af367c1c04..42a70fff1d4 100644 --- a/tests/components/mazda/test_device_tracker.py +++ b/tests/components/mazda/test_device_tracker.py @@ -20,7 +20,7 @@ async def test_device_tracker(hass): state = hass.states.get("device_tracker.my_mazda3_device_tracker") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device Tracker" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device tracker" assert state.attributes.get(ATTR_ICON) == "mdi:car" assert state.attributes.get(ATTR_LATITUDE) == 1.234567 assert state.attributes.get(ATTR_LONGITUDE) == -2.345678 diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index f2e9039397b..763e1490e89 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -32,7 +32,7 @@ async def test_sensors(hass): assert state assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "My Mazda3 Fuel Remaining Percentage" + == "My Mazda3 Fuel remaining percentage" ) assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -46,7 +46,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel Distance Remaining" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel distance remaining" ) assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS @@ -72,7 +72,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_front_left_tire_pressure") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front Left Tire Pressure" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front left tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -88,7 +88,7 @@ async def test_sensors(hass): assert state assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "My Mazda3 Front Right Tire Pressure" + == "My Mazda3 Front right tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -103,7 +103,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_rear_left_tire_pressure") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Tire Pressure" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -118,7 +118,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_rear_right_tire_pressure") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Tire Pressure" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -167,7 +167,7 @@ async def test_electric_vehicle_sensors(hass): # Charge Level state = hass.states.get("sensor.my_mazda3_charge_level") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charge Level" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charge level" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -179,7 +179,7 @@ async def test_electric_vehicle_sensors(hass): # Remaining Range state = hass.states.get("sensor.my_mazda3_remaining_range") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Remaining Range" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Remaining range" assert state.attributes.get(ATTR_ICON) == "mdi:ev-station" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT From 5451ccd2b562acb67e2f97e509612362ab992e0b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 06:05:09 +0200 Subject: [PATCH 337/820] Update wakeonlan to 2.1.0 (#74856) --- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/wake_on_lan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 9cd068ef409..8523231e084 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -6,7 +6,7 @@ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", - "wakeonlan==2.0.1", + "wakeonlan==2.1.0", "async-upnp-client==0.31.2" ], "ssdp": [ diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index e959f4b33f3..fc2c36eed80 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -2,7 +2,7 @@ "domain": "wake_on_lan", "name": "Wake on LAN", "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", - "requirements": ["wakeonlan==2.0.1"], + "requirements": ["wakeonlan==2.1.0"], "codeowners": ["@ntilley905"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index c78dee5d185..55f70e17a2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2415,7 +2415,7 @@ vultr==0.1.2 # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan -wakeonlan==2.0.1 +wakeonlan==2.1.0 # homeassistant.components.wallbox wallbox==0.4.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f82b2d1a25e..d95ea62c203 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1612,7 +1612,7 @@ vultr==0.1.2 # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan -wakeonlan==2.0.1 +wakeonlan==2.1.0 # homeassistant.components.wallbox wallbox==0.4.9 From f4e61eff18a3473bd745ccc13041f5329c570c57 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Jul 2022 21:24:52 -0700 Subject: [PATCH 338/820] Add update coordinator for google calendar (#74690) Co-authored-by: Martin Hjelmare --- homeassistant/components/google/calendar.py | 154 ++++++++++++++------ homeassistant/helpers/update_coordinator.py | 9 ++ tests/components/google/test_calendar.py | 16 +- tests/helpers/test_update_coordinator.py | 29 ++++ 4 files changed, 160 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 79e4b2da114..ca98b3da087 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -2,7 +2,6 @@ from __future__ import annotations -import copy from datetime import datetime, timedelta import logging from typing import Any @@ -21,7 +20,7 @@ from homeassistant.components.calendar import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import ( config_validation as cv, @@ -30,7 +29,11 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from . import ( CONF_IGNORE_AVAILABILITY, @@ -182,9 +185,17 @@ async def async_setup_entry( entity_registry.async_remove( entity_entry.entity_id, ) + coordinator = CalendarUpdateCoordinator( + hass, + calendar_service, + data[CONF_NAME], + calendar_id, + data.get(CONF_SEARCH), + ) + await coordinator.async_config_entry_first_refresh() entities.append( GoogleCalendarEntity( - calendar_service, + coordinator, calendar_id, data, generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass), @@ -213,14 +224,66 @@ async def async_setup_entry( ) -class GoogleCalendarEntity(CalendarEntity): - """A calendar event device.""" +class CalendarUpdateCoordinator(DataUpdateCoordinator): + """Coordinator for calendar RPC calls.""" + + def __init__( + self, + hass: HomeAssistant, + calendar_service: GoogleCalendarService, + name: str, + calendar_id: str, + search: str | None, + ) -> None: + """Create the Calendar event device.""" + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + self.calendar_service = calendar_service + self.calendar_id = calendar_id + self._search = search + + async def async_get_events( + self, start_date: datetime, end_date: datetime + ) -> list[Event]: + """Get all events in a specific time frame.""" + request = ListEventsRequest( + calendar_id=self.calendar_id, + start_time=start_date, + end_time=end_date, + search=self._search, + ) + result_items = [] + try: + result = await self.calendar_service.async_list_events(request) + async for result_page in result: + result_items.extend(result_page.items) + except ApiException as err: + self.async_set_update_error(err) + raise HomeAssistantError(str(err)) from err + return result_items + + async def _async_update_data(self) -> list[Event]: + """Fetch data from API endpoint.""" + request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) + try: + result = await self.calendar_service.async_list_events(request) + except ApiException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + return result.items + + +class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): + """A calendar event entity.""" _attr_has_entity_name = True def __init__( self, - calendar_service: GoogleCalendarService, + coordinator: CalendarUpdateCoordinator, calendar_id: str, data: dict[str, Any], entity_id: str, @@ -228,9 +291,9 @@ class GoogleCalendarEntity(CalendarEntity): entity_enabled: bool, ) -> None: """Create the Calendar event device.""" - self.calendar_service = calendar_service + super().__init__(coordinator) + self.coordinator = coordinator self.calendar_id = calendar_id - self._search: str | None = data.get(CONF_SEARCH) self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) self._event: CalendarEvent | None = None self._attr_name = data[CONF_NAME].capitalize() @@ -240,6 +303,17 @@ class GoogleCalendarEntity(CalendarEntity): self._attr_unique_id = unique_id self._attr_entity_registry_enabled_default = entity_enabled + @property + def should_poll(self) -> bool: + """Enable polling for the entity. + + The coordinator is not used by multiple entities, but instead + is used to poll the calendar API at a separate interval from the + entity state updates itself which happen more frequently (e.g. to + fire an alarm when the next event starts). + """ + return True + @property def extra_state_attributes(self) -> dict[str, bool]: """Return the device state attributes.""" @@ -265,48 +339,44 @@ class GoogleCalendarEntity(CalendarEntity): return True return event.transparency == OPAQUE + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._apply_coordinator_update() + async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - - request = ListEventsRequest( - calendar_id=self.calendar_id, - start_time=start_date, - end_time=end_date, - search=self._search, - ) - result_items = [] - try: - result = await self.calendar_service.async_list_events(request) - async for result_page in result: - result_items.extend(result_page.items) - except ApiException as err: - raise HomeAssistantError(str(err)) from err + result_items = await self.coordinator.async_get_events(start_date, end_date) return [ _get_calendar_event(event) for event in filter(self._event_filter, result_items) ] - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self) -> None: - """Get the latest data.""" - request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) - try: - result = await self.calendar_service.async_list_events(request) - except ApiException as err: - _LOGGER.error("Unable to connect to Google: %s", err) - return + def _apply_coordinator_update(self) -> None: + """Copy state from the coordinator to this entity.""" + events = self.coordinator.data + self._event = _get_calendar_event(next(iter(events))) if events else None + if self._event: + (self._event.summary, self._offset_value) = extract_offset( + self._event.summary, self._offset + ) - # Pick the first visible event and apply offset calculations. - valid_items = filter(self._event_filter, result.items) - event = copy.deepcopy(next(valid_items, None)) - if event: - (event.summary, offset) = extract_offset(event.summary, self._offset) - self._event = _get_calendar_event(event) - self._offset_value = offset - else: - self._event = None + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._apply_coordinator_update() + super()._handle_coordinator_update() + + async def async_update(self) -> None: + """Disable update behavior. + + This relies on the coordinator callback update to write home assistant + state with the next calendar event. This update is a no-op as no new data + fetch is needed to evaluate the state to determine if the next event has + started, handled by CalendarEntity parent class. + """ def _get_calendar_event(event: Event) -> CalendarEvent: @@ -359,7 +429,7 @@ async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) -> raise ValueError("Missing required fields to set start or end date/datetime") try: - await entity.calendar_service.async_create_event( + await entity.coordinator.calendar_service.async_create_event( entity.calendar_id, Event( summary=call.data[EVENT_SUMMARY], diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index fc619469500..30da847642b 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -282,6 +282,15 @@ class DataUpdateCoordinator(Generic[_T]): self.async_update_listeners() + @callback + def async_set_update_error(self, err: Exception) -> None: + """Manually set an error, log the message and notify listeners.""" + self.last_exception = err + if self.last_update_success: + self.logger.error("Error requesting %s data: %s", self.name, err) + self.last_update_success = False + self.async_update_listeners() + @callback def async_set_updated_data(self, data: _T) -> None: """Manually update data, notify listeners and reset refresh interval.""" diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index e49eb0c2e01..f4129eb0926 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -341,7 +341,7 @@ async def test_update_error( assert state.name == TEST_ENTITY_NAME assert state.state == "on" - # Advance time to avoid throttling + # Advance time to next data update interval now += datetime.timedelta(minutes=30) aioclient_mock.clear_requests() @@ -351,12 +351,12 @@ async def test_update_error( async_fire_time_changed(hass, now) await hass.async_block_till_done() - # No change + # Entity is marked uanvailable due to API failure state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME - assert state.state == "on" + assert state.state == "unavailable" - # Advance time beyond update/throttle point + # Advance time past next coordinator update now += datetime.timedelta(minutes=30) aioclient_mock.clear_requests() @@ -380,7 +380,7 @@ async def test_update_error( async_fire_time_changed(hass, now) await hass.async_block_till_done() - # State updated + # State updated with new API response state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME assert state.state == "off" @@ -425,6 +425,10 @@ async def test_http_event_api_failure( response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == "unavailable" + @pytest.mark.freeze_time("2022-03-27 12:05:00+00:00") async def test_http_api_event( @@ -613,7 +617,7 @@ async def test_future_event_update_behavior( # Advance time until event has started now += datetime.timedelta(minutes=60) - now_utc += datetime.timedelta(minutes=30) + now_utc += datetime.timedelta(minutes=60) with patch("homeassistant.util.dt.utcnow", return_value=now_utc), patch( "homeassistant.util.dt.now", return_value=now ): diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 0d0970a4756..4e5f07a2232 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -402,3 +402,32 @@ async def test_not_schedule_refresh_if_system_option_disable_polling(hass): crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) crd.async_add_listener(lambda: None) assert crd._unsub_refresh is None + + +async def test_async_set_update_error(crd, caplog): + """Test manually setting an update failure.""" + update_callback = Mock() + crd.async_add_listener(update_callback) + + crd.async_set_update_error(aiohttp.ClientError("Client Failure #1")) + assert crd.last_update_success is False + assert "Client Failure #1" in caplog.text + update_callback.assert_called_once() + update_callback.reset_mock() + + # Additional failure does not log or change state + crd.async_set_update_error(aiohttp.ClientError("Client Failure #2")) + assert crd.last_update_success is False + assert "Client Failure #2" not in caplog.text + update_callback.assert_not_called() + update_callback.reset_mock() + + crd.async_set_updated_data(200) + assert crd.last_update_success is True + update_callback.assert_called_once() + update_callback.reset_mock() + + crd.async_set_update_error(aiohttp.ClientError("Client Failure #3")) + assert crd.last_update_success is False + assert "Client Failure #2" not in caplog.text + update_callback.assert_called_once() From a3d0719c49c9aeea33eef7d538f82db92c7c83ed Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Mon, 11 Jul 2022 03:11:37 -0400 Subject: [PATCH 339/820] Add binary_sensor to ElkM1 integration (#74485) * Add binary_sensor to ElkM1 integration * Update for review comments. * Fix black. * Fix pylint error. Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/elkm1/__init__.py | 1 + .../components/elkm1/alarm_control_panel.py | 2 +- .../components/elkm1/binary_sensor.py | 57 +++++++++++++++++++ homeassistant/components/elkm1/climate.py | 2 +- homeassistant/components/elkm1/light.py | 2 +- homeassistant/components/elkm1/scene.py | 2 +- homeassistant/components/elkm1/sensor.py | 2 +- homeassistant/components/elkm1/switch.py | 2 +- 9 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/elkm1/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 97f26d8adc5..4e252fb9c92 100644 --- a/.coveragerc +++ b/.coveragerc @@ -266,6 +266,7 @@ omit = homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/__init__.py homeassistant/components/elkm1/alarm_control_panel.py + homeassistant/components/elkm1/binary_sensor.py homeassistant/components/elkm1/climate.py homeassistant/components/elkm1/discovery.py homeassistant/components/elkm1/light.py diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 00fcadfe57a..472d31ccb93 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -74,6 +74,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT, Platform.SCENE, diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 6b6a5b44d55..3f5163a849d 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -69,7 +69,7 @@ async def async_setup_entry( elk = elk_data["elk"] entities: list[ElkEntity] = [] create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities) - async_add_entities(entities, True) + async_add_entities(entities) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/elkm1/binary_sensor.py b/homeassistant/components/elkm1/binary_sensor.py new file mode 100644 index 00000000000..38a72796482 --- /dev/null +++ b/homeassistant/components/elkm1/binary_sensor.py @@ -0,0 +1,57 @@ +"""Support for control of ElkM1 binary sensors.""" +from __future__ import annotations + +from typing import Any + +from elkm1_lib.const import ZoneLogicalStatus, ZoneType +from elkm1_lib.elements import Element +from elkm1_lib.zones import Zone + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ElkAttachedEntity, ElkEntity +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Create the Elk-M1 sensor platform.""" + + elk_data = hass.data[DOMAIN][config_entry.entry_id] + auto_configure = elk_data["auto_configure"] + elk = elk_data["elk"] + + entities: list[ElkEntity] = [] + for element in elk.zones: + # Don't create binary sensors for zones that are analog + if element.definition in {ZoneType.TEMPERATURE, ZoneType.ANALOG_ZONE}: + continue + + if auto_configure: + if not element.configured: + continue + elif not elk_data["config"]["zone"]["included"][element.index]: + continue + + entities.append(ElkBinarySensor(element, elk, elk_data)) + + async_add_entities(entities) + + +class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity): + """Representation of ElkM1 binary sensor.""" + + _element: Zone + _attr_entity_registry_enabled_default = False + + def _element_changed(self, _: Element, changeset: Any) -> None: + # Zone in NORMAL state is OFF; any other state is ON + self._attr_is_on = bool( + self._element.logical_status != ZoneLogicalStatus.NORMAL + ) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 9f6dc359f6f..8bbf776c475 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -68,7 +68,7 @@ async def async_setup_entry( create_elk_entities( elk_data, elk.thermostats, "thermostat", ElkThermostat, entities ) - async_add_entities(entities, True) + async_add_entities(entities) class ElkThermostat(ElkEntity, ClimateEntity): diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py index 9e008359e8c..3db457761aa 100644 --- a/homeassistant/components/elkm1/light.py +++ b/homeassistant/components/elkm1/light.py @@ -26,7 +26,7 @@ async def async_setup_entry( entities: list[ElkEntity] = [] elk = elk_data["elk"] create_elk_entities(elk_data, elk.lights, "plc", ElkLight, entities) - async_add_entities(entities, True) + async_add_entities(entities) class ElkLight(ElkEntity, LightEntity): diff --git a/homeassistant/components/elkm1/scene.py b/homeassistant/components/elkm1/scene.py index d8100c5bcb1..1869e5ba0f3 100644 --- a/homeassistant/components/elkm1/scene.py +++ b/homeassistant/components/elkm1/scene.py @@ -24,7 +24,7 @@ async def async_setup_entry( entities: list[ElkEntity] = [] elk = elk_data["elk"] create_elk_entities(elk_data, elk.tasks, "task", ElkTask, entities) - async_add_entities(entities, True) + async_add_entities(entities) class ElkTask(ElkAttachedEntity, Scene): diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 57f989d5cb5..1d84af259ee 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -51,7 +51,7 @@ async def async_setup_entry( create_elk_entities(elk_data, [elk.panel], "panel", ElkPanel, entities) create_elk_entities(elk_data, elk.settings, "setting", ElkSetting, entities) create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities) - async_add_entities(entities, True) + async_add_entities(entities) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py index 54588958e61..a17557b1507 100644 --- a/homeassistant/components/elkm1/switch.py +++ b/homeassistant/components/elkm1/switch.py @@ -24,7 +24,7 @@ async def async_setup_entry( entities: list[ElkEntity] = [] elk = elk_data["elk"] create_elk_entities(elk_data, elk.outputs, "output", ElkOutput, entities) - async_add_entities(entities, True) + async_add_entities(entities) class ElkOutput(ElkAttachedEntity, SwitchEntity): From eb92f0e16c7d462b83d9a2bde6ebd266e9c63c79 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 09:44:56 +0200 Subject: [PATCH 340/820] Migrate Forecast.Solar to new entity naming style (#74898) --- .../components/forecast_solar/const.py | 20 ++++++------ .../components/forecast_solar/sensor.py | 3 +- .../components/forecast_solar/test_sensor.py | 31 ++++++++++++------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index d9742cf5dfc..185025fb5ce 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -19,31 +19,31 @@ CONF_INVERTER_SIZE = "inverter_size" SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( key="energy_production_today", - name="Estimated Energy Production - Today", + name="Estimated energy production - today", state=lambda estimate: estimate.energy_production_today / 1000, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="energy_production_tomorrow", - name="Estimated Energy Production - Tomorrow", + name="Estimated energy production - tomorrow", state=lambda estimate: estimate.energy_production_tomorrow / 1000, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_today", - name="Highest Power Peak Time - Today", + name="Highest power peak time - today", device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_tomorrow", - name="Highest Power Peak Time - Tomorrow", + name="Highest power peak time - tomorrow", device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_production_now", - name="Estimated Power Production - Now", + name="Estimated power production - now", device_class=SensorDeviceClass.POWER, state=lambda estimate: estimate.power_production_now, state_class=SensorStateClass.MEASUREMENT, @@ -54,7 +54,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=1) ), - name="Estimated Power Production - Next Hour", + name="Estimated power production - next hour", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, @@ -64,7 +64,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=12) ), - name="Estimated Power Production - Next 12 Hours", + name="Estimated power production - next 12 hours", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, @@ -74,14 +74,14 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=24) ), - name="Estimated Power Production - Next 24 Hours", + name="Estimated power production - next 24 hours", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="energy_current_hour", - name="Estimated Energy Production - This Hour", + name="Estimated energy production - this hour", state=lambda estimate: estimate.energy_current_hour / 1000, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, @@ -89,7 +89,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( key="energy_next_hour", state=lambda estimate: estimate.sum_energy_production(1) / 1000, - name="Estimated Energy Production - Next Hour", + name="Estimated energy production - next hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 78335292a78..7bac69b1b6e 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -39,6 +39,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): """Defines a Forecast.Solar sensor.""" entity_description: ForecastSolarSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -58,7 +59,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, entry_id)}, manufacturer="Forecast.Solar", model=coordinator.data.account_type.value, - name="Solar Production Forecast", + name="Solar production forecast", configuration_url="https://forecast.solar", ) diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index a05acb5bc16..893730c722e 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensors( assert state.state == "100.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - Today" + == "Solar production forecast Estimated energy production - today" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -56,7 +56,7 @@ async def test_sensors( assert state.state == "200.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - Tomorrow" + == "Solar production forecast Estimated energy production - tomorrow" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -69,7 +69,10 @@ async def test_sensors( assert state assert entry.unique_id == f"{entry_id}_power_highest_peak_time_today" assert state.state == "2021-06-27T20:00:00+00:00" # Timestamp sensor is UTC - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Today" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Solar production forecast Highest power peak time - today" + ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -82,7 +85,8 @@ async def test_sensors( assert entry.unique_id == f"{entry_id}_power_highest_peak_time_tomorrow" assert state.state == "2021-06-27T21:00:00+00:00" # Timestamp sensor is UTC assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Tomorrow" + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Solar production forecast Highest power peak time - tomorrow" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP @@ -96,7 +100,8 @@ async def test_sensors( assert entry.unique_id == f"{entry_id}_power_production_now" assert state.state == "300000" assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Power Production - Now" + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Solar production forecast Estimated power production - now" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -111,7 +116,7 @@ async def test_sensors( assert state.state == "800.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - This Hour" + == "Solar production forecast Estimated energy production - this hour" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -126,7 +131,7 @@ async def test_sensors( assert state.state == "900.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - Next Hour" + == "Solar production forecast Estimated energy production - next hour" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -138,7 +143,7 @@ async def test_sensors( assert device_entry assert device_entry.identifiers == {(DOMAIN, f"{entry_id}")} assert device_entry.manufacturer == "Forecast.Solar" - assert device_entry.name == "Solar Production Forecast" + assert device_entry.name == "Solar production forecast" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "public" assert not device_entry.sw_version @@ -172,17 +177,17 @@ async def test_disabled_by_default( [ ( "power_production_next_12hours", - "Estimated Power Production - Next 12 Hours", + "Estimated power production - next 12 hours", "600000", ), ( "power_production_next_24hours", - "Estimated Power Production - Next 24 Hours", + "Estimated power production - next 24 hours", "700000", ), ( "power_production_next_hour", - "Estimated Power Production - Next Hour", + "Estimated power production - next hour", "400000", ), ], @@ -219,7 +224,9 @@ async def test_enabling_disable_by_default( assert state assert entry.unique_id == f"{entry_id}_{key}" assert state.state == value - assert state.attributes.get(ATTR_FRIENDLY_NAME) == name + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == f"Solar production forecast {name}" + ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER From 34b8f2b28379a774851e5e919646cf952a062b2e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 09:51:01 +0200 Subject: [PATCH 341/820] Migrate Ambee to new entity naming style (#74877) --- homeassistant/components/ambee/const.py | 54 +++++++++--------- homeassistant/components/ambee/sensor.py | 2 + tests/components/ambee/test_sensor.py | 73 ++++++++++++++---------- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index 8f8f2237654..83abb841629 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -27,7 +27,7 @@ SERVICE_AIR_QUALITY: Final = "air_quality" SERVICE_POLLEN: Final = "pollen" SERVICES: dict[str, str] = { - SERVICE_AIR_QUALITY: "Air Quality", + SERVICE_AIR_QUALITY: "Air quality", SERVICE_POLLEN: "Pollen", } @@ -35,25 +35,25 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { SERVICE_AIR_QUALITY: [ SensorEntityDescription( key="particulate_matter_2_5", - name="Particulate Matter < 2.5 μm", + name="Particulate matter < 2.5 μm", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="particulate_matter_10", - name="Particulate Matter < 10 μm", + name="Particulate matter < 10 μm", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="sulphur_dioxide", - name="Sulphur Dioxide (SO2)", + name="Sulphur dioxide (SO2)", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="nitrogen_dioxide", - name="Nitrogen Dioxide (NO2)", + name="Nitrogen dioxide (NO2)", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=SensorStateClass.MEASUREMENT, ), @@ -65,60 +65,60 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="carbon_monoxide", - name="Carbon Monoxide (CO)", + name="Carbon monoxide (CO)", device_class=SensorDeviceClass.CO, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="air_quality_index", - name="Air Quality Index (AQI)", + name="Air quality index (AQI)", state_class=SensorStateClass.MEASUREMENT, ), ], SERVICE_POLLEN: [ SensorEntityDescription( key="grass", - name="Grass Pollen", + name="Grass", icon="mdi:grass", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="tree", - name="Tree Pollen", + name="Tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="weed", - name="Weed Pollen", + name="Weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="grass_risk", - name="Grass Pollen Risk", + name="Grass risk", icon="mdi:grass", device_class=DEVICE_CLASS_AMBEE_RISK, ), SensorEntityDescription( key="tree_risk", - name="Tree Pollen Risk", + name="Tree risk", icon="mdi:tree", device_class=DEVICE_CLASS_AMBEE_RISK, ), SensorEntityDescription( key="weed_risk", - name="Weed Pollen Risk", + name="Weed risk", icon="mdi:sprout", device_class=DEVICE_CLASS_AMBEE_RISK, ), SensorEntityDescription( key="grass_poaceae", - name="Poaceae Grass Pollen", + name="Poaceae grass", icon="mdi:grass", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -126,7 +126,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_alder", - name="Alder Tree Pollen", + name="Alder tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -134,7 +134,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_birch", - name="Birch Tree Pollen", + name="Birch tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -142,7 +142,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_cypress", - name="Cypress Tree Pollen", + name="Cypress tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -150,7 +150,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_elm", - name="Elm Tree Pollen", + name="Elm tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -158,7 +158,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_hazel", - name="Hazel Tree Pollen", + name="Hazel tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -166,7 +166,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_oak", - name="Oak Tree Pollen", + name="Oak tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -174,7 +174,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_pine", - name="Pine Tree Pollen", + name="Pine tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -182,7 +182,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_plane", - name="Plane Tree Pollen", + name="Plane tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -190,7 +190,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_poplar", - name="Poplar Tree Pollen", + name="Poplar tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -198,7 +198,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_chenopod", - name="Chenopod Weed Pollen", + name="Chenopod weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -206,7 +206,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_mugwort", - name="Mugwort Weed Pollen", + name="Mugwort weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -214,7 +214,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_nettle", - name="Nettle Weed Pollen", + name="Nettle weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -222,7 +222,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_ragweed", - name="Ragweed Weed Pollen", + name="Ragweed weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index bf9cfe74f31..8fb6c9f2a61 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -42,6 +42,8 @@ async def async_setup_entry( class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): """Defines an Ambee sensor.""" + _attr_has_entity_name = True + def __init__( self, *, diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py index a32398139d0..d143aea8f7c 100644 --- a/tests/components/ambee/test_sensor.py +++ b/tests/components/ambee/test_sensor.py @@ -41,7 +41,10 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_2_5" assert state.state == "3.14" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Air quality Particulate matter < 2.5 μm" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -56,7 +59,10 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_10" assert state.state == "5.24" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Air quality Particulate matter < 10 μm" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -71,7 +77,9 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_sulphur_dioxide" assert state.state == "0.031" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Sulphur dioxide (SO2)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -86,7 +94,9 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_nitrogen_dioxide" assert state.state == "0.66" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Nitrogen dioxide (NO2)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -101,7 +111,7 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_ozone" assert state.state == "17.067" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Ozone" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -117,7 +127,9 @@ async def test_air_quality( assert entry.unique_id == f"{entry_id}_air_quality_carbon_monoxide" assert state.state == "0.105" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Carbon Monoxide (CO)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Carbon monoxide (CO)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -131,7 +143,10 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_air_quality_index" assert state.state == "13" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Air quality Air quality index (AQI)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -142,7 +157,7 @@ async def test_air_quality( assert device_entry assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_air_quality")} assert device_entry.manufacturer == "Ambee" - assert device_entry.name == "Air Quality" + assert device_entry.name == "Air quality" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert not device_entry.model assert not device_entry.sw_version @@ -163,7 +178,7 @@ async def test_pollen( assert state assert entry.unique_id == f"{entry_id}_pollen_grass" assert state.state == "190" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Grass" assert state.attributes.get(ATTR_ICON) == "mdi:grass" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( @@ -178,7 +193,7 @@ async def test_pollen( assert state assert entry.unique_id == f"{entry_id}_pollen_tree" assert state.state == "127" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Tree" assert state.attributes.get(ATTR_ICON) == "mdi:tree" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( @@ -193,7 +208,7 @@ async def test_pollen( assert state assert entry.unique_id == f"{entry_id}_pollen_weed" assert state.state == "95" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Weed" assert state.attributes.get(ATTR_ICON) == "mdi:sprout" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( @@ -209,7 +224,7 @@ async def test_pollen( assert entry.unique_id == f"{entry_id}_pollen_grass_risk" assert state.state == "high" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen Risk" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Grass risk" assert state.attributes.get(ATTR_ICON) == "mdi:grass" assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -221,7 +236,7 @@ async def test_pollen( assert entry.unique_id == f"{entry_id}_pollen_tree_risk" assert state.state == "moderate" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen Risk" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Tree risk" assert state.attributes.get(ATTR_ICON) == "mdi:tree" assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -233,7 +248,7 @@ async def test_pollen( assert entry.unique_id == f"{entry_id}_pollen_weed_risk" assert state.state == "high" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen Risk" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Weed risk" assert state.attributes.get(ATTR_ICON) == "mdi:sprout" assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -286,20 +301,20 @@ async def test_pollen_disabled_by_default( @pytest.mark.parametrize( "key,icon,name,value", [ - ("grass_poaceae", "mdi:grass", "Poaceae Grass Pollen", "190"), - ("tree_alder", "mdi:tree", "Alder Tree Pollen", "0"), - ("tree_birch", "mdi:tree", "Birch Tree Pollen", "35"), - ("tree_cypress", "mdi:tree", "Cypress Tree Pollen", "0"), - ("tree_elm", "mdi:tree", "Elm Tree Pollen", "0"), - ("tree_hazel", "mdi:tree", "Hazel Tree Pollen", "0"), - ("tree_oak", "mdi:tree", "Oak Tree Pollen", "55"), - ("tree_pine", "mdi:tree", "Pine Tree Pollen", "30"), - ("tree_plane", "mdi:tree", "Plane Tree Pollen", "5"), - ("tree_poplar", "mdi:tree", "Poplar Tree Pollen", "0"), - ("weed_chenopod", "mdi:sprout", "Chenopod Weed Pollen", "0"), - ("weed_mugwort", "mdi:sprout", "Mugwort Weed Pollen", "1"), - ("weed_nettle", "mdi:sprout", "Nettle Weed Pollen", "88"), - ("weed_ragweed", "mdi:sprout", "Ragweed Weed Pollen", "3"), + ("grass_poaceae", "mdi:grass", "Poaceae grass", "190"), + ("tree_alder", "mdi:tree", "Alder tree", "0"), + ("tree_birch", "mdi:tree", "Birch tree", "35"), + ("tree_cypress", "mdi:tree", "Cypress tree", "0"), + ("tree_elm", "mdi:tree", "Elm tree", "0"), + ("tree_hazel", "mdi:tree", "Hazel tree", "0"), + ("tree_oak", "mdi:tree", "Oak tree", "55"), + ("tree_pine", "mdi:tree", "Pine tree", "30"), + ("tree_plane", "mdi:tree", "Plane tree", "5"), + ("tree_poplar", "mdi:tree", "Poplar tree", "0"), + ("weed_chenopod", "mdi:sprout", "Chenopod weed", "0"), + ("weed_mugwort", "mdi:sprout", "Mugwort weed", "1"), + ("weed_nettle", "mdi:sprout", "Nettle weed", "88"), + ("weed_ragweed", "mdi:sprout", "Ragweed weed", "3"), ], ) async def test_pollen_enable_disable_by_defaults( @@ -335,7 +350,7 @@ async def test_pollen_enable_disable_by_defaults( assert state assert entry.unique_id == f"{entry_id}_pollen_{key}" assert state.state == value - assert state.attributes.get(ATTR_FRIENDLY_NAME) == name + assert state.attributes.get(ATTR_FRIENDLY_NAME) == f"Pollen {name}" assert state.attributes.get(ATTR_ICON) == icon assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( From 81f74d205340ed10a1b690dc7f8ecf40e9590e74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 10:42:23 +0200 Subject: [PATCH 342/820] Update pre-commit to 2.20.0 (#74955) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index dbb7a76dac4..fa0dcb68a09 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ coverage==6.4.1 freezegun==1.2.1 mock-open==1.4.0 mypy==0.961 -pre-commit==2.19.0 +pre-commit==2.20.0 pylint==2.14.4 pipdeptree==2.2.1 pytest-aiohttp==0.3.0 From 6e1cd4c48a62458a2adce1a1b07932b99cb4e52d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 10:49:06 +0200 Subject: [PATCH 343/820] Migrate ecobee to native_* (#74043) --- homeassistant/components/ecobee/weather.py | 43 +++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 1c330ceb4e2..aca4dcdf2f5 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -7,20 +7,24 @@ from pyecobee.const import ECOBEE_STATE_UNKNOWN from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_FAHRENHEIT +from homeassistant.const import ( + LENGTH_METERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from homeassistant.util.pressure import convert as pressure_convert from .const import ( DOMAIN, @@ -49,6 +53,11 @@ async def async_setup_entry( class EcobeeWeather(WeatherEntity): """Representation of Ecobee weather data.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_FAHRENHEIT + _attr_native_visibility_unit = LENGTH_METERS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + def __init__(self, data, name, index): """Initialize the Ecobee weather platform.""" self.data = data @@ -101,7 +110,7 @@ class EcobeeWeather(WeatherEntity): return None @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" try: return float(self.get_forecast(0, "temperature")) / 10 @@ -109,18 +118,10 @@ class EcobeeWeather(WeatherEntity): return None @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" try: pressure = self.get_forecast(0, "pressure") - if not self.hass.config.units.is_metric: - pressure = pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG) - return round(pressure, 2) return round(pressure) except ValueError: return None @@ -134,15 +135,15 @@ class EcobeeWeather(WeatherEntity): return None @property - def visibility(self): + def native_visibility(self): """Return the visibility.""" try: - return int(self.get_forecast(0, "visibility")) / 1000 + return int(self.get_forecast(0, "visibility")) except ValueError: return None @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" try: return int(self.get_forecast(0, "windSpeed")) @@ -202,13 +203,13 @@ def _process_forecast(json): json["weatherSymbol"] ] if json["tempHigh"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_TEMP] = float(json["tempHigh"]) / 10 + forecast[ATTR_FORECAST_NATIVE_TEMP] = float(json["tempHigh"]) / 10 if json["tempLow"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_TEMP_LOW] = float(json["tempLow"]) / 10 + forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = float(json["tempLow"]) / 10 if json["windBearing"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"]) if json["windSpeed"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_WIND_SPEED] = int(json["windSpeed"]) + forecast[ATTR_FORECAST_NATIVE_WIND_SPEED] = int(json["windSpeed"]) except (ValueError, IndexError, KeyError): return None From ed1545fab06708503c990ece79c3b085478fc3b1 Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 11 Jul 2022 10:49:31 +0200 Subject: [PATCH 344/820] Fix Pyload request content type headers (#74957) --- homeassistant/components/pyload/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index e4bbf7df3d2..81e1a02408a 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -144,7 +143,7 @@ class PyLoadAPI: """Initialize pyLoad API and set headers needed later.""" self.api_url = api_url self.status = None - self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON} + self.headers = {"Content-Type": CONTENT_TYPE_JSON} if username is not None and password is not None: self.payload = {"username": username, "password": password} From f6fd7d115e6813d7980a6521919e0021f6415b94 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 10:58:57 +0200 Subject: [PATCH 345/820] Migrate homematicip_cloud to native_* (#74385) --- .../components/homematicip_cloud/weather.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 985754a8417..3acbae95441 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -22,7 +22,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -71,6 +71,9 @@ async def async_setup_entry( class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): """Representation of the HomematicIP weather sensor plus & basic.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the weather sensor.""" super().__init__(hap, device) @@ -81,22 +84,17 @@ class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): return self._device.label @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the platform temperature.""" return self._device.actualTemperature - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self) -> int: """Return the humidity.""" return self._device.humidity @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return self._device.windSpeed @@ -129,6 +127,9 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity): """Representation of the HomematicIP home weather.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, hap: HomematicipHAP) -> None: """Initialize the home weather.""" hap.home.modelType = "HmIP-Home-Weather" @@ -145,22 +146,17 @@ class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity): return f"Weather {self._home.location.city}" @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the temperature.""" return self._device.weather.temperature - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self) -> int: """Return the humidity.""" return self._device.weather.humidity @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return round(self._device.weather.windSpeed, 1) From 7d27dad1902336e959fab97db2e6e5bad33520d3 Mon Sep 17 00:00:00 2001 From: henryptung Date: Mon, 11 Jul 2022 02:33:28 -0700 Subject: [PATCH 346/820] Remove pip --prefix workaround (#74922) Remove --prefix workaround See discussion in https://github.com/home-assistant/core/issues/74405. This workaround is no longer needed on pip >= 21.0 and actively causes problems for pip >= 21.3. --- homeassistant/util/package.py | 3 --- tests/util/test_package.py | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index aad93e37542..18ab43967ec 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -89,9 +89,6 @@ def install_package( # This only works if not running in venv args += ["--user"] env["PYTHONUSERBASE"] = os.path.abspath(target) - # Workaround for incompatible prefix setting - # See http://stackoverflow.com/a/4495175 - args += ["--prefix="] _LOGGER.debug("Running pip command: args=%s", args) with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) as process: _, stderr = process.communicate() diff --git a/tests/util/test_package.py b/tests/util/test_package.py index d6b7402a5b6..7ab087f1463 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -137,7 +137,6 @@ def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv): "--quiet", TEST_NEW_REQ, "--user", - "--prefix=", ] assert package.install_package(TEST_NEW_REQ, False, target=target) @@ -156,7 +155,7 @@ def test_install_target_venv(mock_sys, mock_popen, mock_env_copy, mock_venv): def test_install_error(caplog, mock_sys, mock_popen, mock_venv): - """Test an install with a target.""" + """Test an install that errors out.""" caplog.set_level(logging.WARNING) mock_popen.return_value.returncode = 1 assert not package.install_package(TEST_NEW_REQ) From 0d6bf08ff69a0b75ffe797f49c201d1b7c4f3a72 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 11:59:06 +0200 Subject: [PATCH 347/820] Don't allow using deprecated features of WeatherEntity (#74394) --- homeassistant/components/weather/__init__.py | 37 ++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 1fdb9173646..3e0233917d7 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -171,16 +171,16 @@ class Forecast(TypedDict, total=False): datetime: str precipitation_probability: int | None native_precipitation: float | None - precipitation: float | None + precipitation: None native_pressure: float | None - pressure: float | None + pressure: None native_temperature: float | None - temperature: float | None + temperature: None native_templow: float | None - templow: float | None + templow: None wind_bearing: float | str | None native_wind_speed: float | None - wind_speed: float | None + wind_speed: None async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -218,33 +218,33 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_precision: float - _attr_pressure: float | None = ( + _attr_pressure: None = ( None # Provide backwards compatibility. Use _attr_native_pressure ) - _attr_pressure_unit: str | None = ( + _attr_pressure_unit: None = ( None # Provide backwards compatibility. Use _attr_native_pressure_unit ) _attr_state: None = None - _attr_temperature: float | None = ( + _attr_temperature: None = ( None # Provide backwards compatibility. Use _attr_native_temperature ) - _attr_temperature_unit: str | None = ( + _attr_temperature_unit: None = ( None # Provide backwards compatibility. Use _attr_native_temperature_unit ) - _attr_visibility: float | None = ( + _attr_visibility: None = ( None # Provide backwards compatibility. Use _attr_native_visibility ) - _attr_visibility_unit: str | None = ( + _attr_visibility_unit: None = ( None # Provide backwards compatibility. Use _attr_native_visibility_unit ) - _attr_precipitation_unit: str | None = ( + _attr_precipitation_unit: None = ( None # Provide backwards compatibility. Use _attr_native_precipitation_unit ) _attr_wind_bearing: float | str | None = None - _attr_wind_speed: float | None = ( + _attr_wind_speed: None = ( None # Provide backwards compatibility. Use _attr_native_wind_speed ) - _attr_wind_speed_unit: str | None = ( + _attr_wind_speed_unit: None = ( None # Provide backwards compatibility. Use _attr_native_wind_speed_unit ) @@ -321,6 +321,7 @@ class WeatherEntity(Entity): return self.async_registry_entry_updated() + @final @property def temperature(self) -> float | None: """Return the temperature for backward compatibility. @@ -345,6 +346,7 @@ class WeatherEntity(Entity): return self._attr_native_temperature_unit + @final @property def temperature_unit(self) -> str | None: """Return the temperature unit for backward compatibility. @@ -376,6 +378,7 @@ class WeatherEntity(Entity): return self._default_temperature_unit + @final @property def pressure(self) -> float | None: """Return the pressure for backward compatibility. @@ -400,6 +403,7 @@ class WeatherEntity(Entity): return self._attr_native_pressure_unit + @final @property def pressure_unit(self) -> str | None: """Return the pressure unit for backward compatibility. @@ -436,6 +440,7 @@ class WeatherEntity(Entity): """Return the humidity in native units.""" return self._attr_humidity + @final @property def wind_speed(self) -> float | None: """Return the wind_speed for backward compatibility. @@ -460,6 +465,7 @@ class WeatherEntity(Entity): return self._attr_native_wind_speed_unit + @final @property def wind_speed_unit(self) -> str | None: """Return the wind_speed unit for backward compatibility. @@ -505,6 +511,7 @@ class WeatherEntity(Entity): """Return the ozone level.""" return self._attr_ozone + @final @property def visibility(self) -> float | None: """Return the visibility for backward compatibility. @@ -529,6 +536,7 @@ class WeatherEntity(Entity): return self._attr_native_visibility_unit + @final @property def visibility_unit(self) -> str | None: """Return the visibility unit for backward compatibility. @@ -573,6 +581,7 @@ class WeatherEntity(Entity): return self._attr_native_precipitation_unit + @final @property def precipitation_unit(self) -> str | None: """Return the precipitation unit for backward compatibility. From c80066072cc487e4aba20caf674cab0231a41105 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 12:18:28 +0200 Subject: [PATCH 348/820] Update psutil to 5.9.1 (#74963) --- homeassistant/components/systemmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index b9fe86cbfa8..398614815a2 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -2,7 +2,7 @@ "domain": "systemmonitor", "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": ["psutil==5.9.0"], + "requirements": ["psutil==5.9.1"], "codeowners": [], "iot_class": "local_push", "loggers": ["psutil"] diff --git a/requirements_all.txt b/requirements_all.txt index 55f70e17a2d..75da41fcdb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1284,7 +1284,7 @@ prometheus_client==0.7.1 proxmoxer==1.3.1 # homeassistant.components.systemmonitor -psutil==5.9.0 +psutil==5.9.1 # homeassistant.components.pulseaudio_loopback pulsectl==20.2.4 From ca93aacc5751e51363b32be0925e5859626f48bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Mon, 11 Jul 2022 12:24:37 +0200 Subject: [PATCH 349/820] Add blebox tvLiftBox support (#74395) * Added tvLiftBox support. * Changes after @epenet code review. * After @epenet code review, dictionaries moved to relevant modules. * Import path changed to full path. * Removed redundant code in BLEBOX_TO__DEVICE_CLASSES for switch and button platforms. * Post isort on covers. * Added tests, required version bump. As property was added inside dependency. --- homeassistant/components/blebox/__init__.py | 7 +- homeassistant/components/blebox/button.py | 48 +++++++++++++ homeassistant/components/blebox/const.py | 14 ---- homeassistant/components/blebox/cover.py | 11 ++- homeassistant/components/blebox/manifest.json | 2 +- homeassistant/components/blebox/sensor.py | 10 ++- homeassistant/components/blebox/switch.py | 5 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/blebox/test_button.py | 68 +++++++++++++++++++ 10 files changed, 141 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/blebox/button.py create mode 100644 tests/components/blebox/test_button.py diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 383e5a95f4e..ff907d728b7 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -17,12 +17,13 @@ from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT _LOGGER = logging.getLogger(__name__) PLATFORMS = [ + Platform.AIR_QUALITY, + Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, + Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, - Platform.AIR_QUALITY, - Platform.LIGHT, - Platform.CLIMATE, ] PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py new file mode 100644 index 00000000000..01de5fa56b7 --- /dev/null +++ b/homeassistant/components/blebox/button.py @@ -0,0 +1,48 @@ +"""BleBox button entities implementation.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import BleBoxEntity, create_blebox_entities + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a BleBox button entry.""" + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxButtonEntity, "buttons" + ) + + +class BleBoxButtonEntity(BleBoxEntity, ButtonEntity): + """Representation of BleBox buttons.""" + + def __init__(self, feature): + """Initialize a BleBox button feature.""" + super().__init__(feature) + self._attr_device_class = ButtonDeviceClass.UPDATE + self._attr_icon = self.get_icon() + + def get_icon(self): + """Return icon for endpoint.""" + if "up" in self._feature.query_string: + return "mdi:arrow-up-circle" + if "down" in self._feature.query_string: + return "mdi:arrow-down-circle" + if "fav" in self._feature.query_string: + return "mdi:heart-circle" + if "open" in self._feature.query_string: + return "mdi:arrow-up-circle" + if "close" in self._feature.query_string: + return "mdi:arrow-down-circle" + return "" + + async def async_press(self) -> None: + """Handle the button press.""" + await self._feature.set() diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index 013a6501068..533c33f37bb 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -1,9 +1,5 @@ """Constants for the BleBox devices integration.""" -from homeassistant.components.cover import CoverDeviceClass -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.components.switch import SwitchDeviceClass -from homeassistant.const import TEMP_CELSIUS DOMAIN = "blebox" PRODUCT = "product" @@ -16,16 +12,6 @@ CANNOT_CONNECT = "cannot_connect" UNSUPPORTED_VERSION = "unsupported_version" UNKNOWN = "unknown" -BLEBOX_TO_HASS_DEVICE_CLASSES = { - "shutter": CoverDeviceClass.SHUTTER, - "gatebox": CoverDeviceClass.DOOR, - "gate": CoverDeviceClass.GATE, - "relay": SwitchDeviceClass.SWITCH, - "temperature": SensorDeviceClass.TEMPERATURE, -} - - -BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} DEFAULT_HOST = "192.168.0.2" DEFAULT_PORT = 80 diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 8763ec34d34..882356f1a77 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -5,6 +5,7 @@ from typing import Any from homeassistant.components.cover import ( ATTR_POSITION, + CoverDeviceClass, CoverEntity, CoverEntityFeature, ) @@ -14,7 +15,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_DEVICE_CLASSES + +BLEBOX_TO_COVER_DEVICE_CLASSES = { + "gate": CoverDeviceClass.GATE, + "gatebox": CoverDeviceClass.DOOR, + "shutter": CoverDeviceClass.SHUTTER, +} + BLEBOX_TO_HASS_COVER_STATES = { None: None, @@ -49,7 +56,7 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): def __init__(self, feature): """Initialize a BleBox cover feature.""" super().__init__(feature) - self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + self._attr_device_class = BLEBOX_TO_COVER_DEVICE_CLASSES[feature.device_class] position = CoverEntityFeature.SET_POSITION if feature.is_slider else 0 stop = CoverEntityFeature.STOP if feature.has_stop else 0 self._attr_supported_features = ( diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 08554695316..49d44db8f01 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.0.1"], + "requirements": ["blebox_uniapi==2.0.2"], "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 2c317acada8..663af970e3e 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -1,11 +1,15 @@ """BleBox sensor entities.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_DEVICE_CLASSES, BLEBOX_TO_UNIT_MAP + +BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} + +BLEBOX_TO_SENSOR_DEVICE_CLASS = {"temperature": SensorDeviceClass.TEMPERATURE} async def async_setup_entry( @@ -27,7 +31,7 @@ class BleBoxSensorEntity(BleBoxEntity, SensorEntity): """Initialize a BleBox sensor feature.""" super().__init__(feature) self._attr_native_unit_of_measurement = BLEBOX_TO_UNIT_MAP[feature.unit] - self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + self._attr_device_class = BLEBOX_TO_SENSOR_DEVICE_CLASS[feature.device_class] @property def native_value(self): diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index 50eba1d2c4a..f9c866244c7 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -1,13 +1,12 @@ """BleBox switch implementation.""" from datetime import timedelta -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_DEVICE_CLASSES SCAN_INTERVAL = timedelta(seconds=5) @@ -29,7 +28,7 @@ class BleBoxSwitchEntity(BleBoxEntity, SwitchEntity): def __init__(self, feature): """Initialize a BleBox switch feature.""" super().__init__(feature) - self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + self._attr_device_class = SwitchDeviceClass.SWITCH @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 75da41fcdb9..7a9c482ea84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ bizkaibus==0.1.1 bleak==0.14.3 # homeassistant.components.blebox -blebox_uniapi==2.0.1 +blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d95ea62c203..5b7077c8ebd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ bimmer_connected==0.9.6 bleak==0.14.3 # homeassistant.components.blebox -blebox_uniapi==2.0.1 +blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/tests/components/blebox/test_button.py b/tests/components/blebox/test_button.py new file mode 100644 index 00000000000..22e1b8cb734 --- /dev/null +++ b/tests/components/blebox/test_button.py @@ -0,0 +1,68 @@ +"""Blebox button entities tests.""" +import logging +from unittest.mock import PropertyMock + +import blebox_uniapi +import pytest + +from homeassistant.components.button import ButtonDeviceClass +from homeassistant.const import ATTR_ICON + +from .conftest import async_setup_entity, mock_feature + +query_icon_matching = [ + ("up", "mdi:arrow-up-circle"), + ("down", "mdi:arrow-down-circle"), + ("fav", "mdi:heart-circle"), + ("open", "mdi:arrow-up-circle"), + ("close", "mdi:arrow-down-circle"), +] + + +@pytest.fixture(name="tvliftbox") +def tv_lift_box_fixture(caplog): + """Return simple button entity mock.""" + caplog.set_level(logging.ERROR) + + feature = mock_feature( + "buttons", + blebox_uniapi.button.Button, + unique_id="BleBox-tvLiftBox-4a3fdaad90aa-open_or_stop", + full_name="tvLiftBox-open_or_stop", + control_type=blebox_uniapi.button.ControlType.OPEN, + ) + + product = feature.product + type(product).name = PropertyMock(return_value="My tvLiftBox") + type(product).model = PropertyMock(return_value="tvLiftBox") + type(product)._query_string = PropertyMock(return_value="open_or_stop") + + return (feature, "button.tvliftbox_open_or_stop") + + +async def test_tvliftbox_init(tvliftbox, hass, config, caplog): + """Test tvLiftBox initialisation.""" + caplog.set_level(logging.ERROR) + + _, entity_id = tvliftbox + entry = await async_setup_entity(hass, config, entity_id) + state = hass.states.get(entity_id) + + assert entry.unique_id == "BleBox-tvLiftBox-4a3fdaad90aa-open_or_stop" + + assert state.attributes["device_class"] == ButtonDeviceClass.UPDATE + + assert state.name == "tvLiftBox-open_or_stop" + + +@pytest.mark.parametrize("input", query_icon_matching) +async def test_get_icon(input, tvliftbox, hass, config, caplog): + """Test if proper icon is returned.""" + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = tvliftbox + feature_mock.query_string = input[0] + _ = await async_setup_entity(hass, config, entity_id) + state = hass.states.get(entity_id) + + assert state.attributes[ATTR_ICON] == input[1] From ba3f287c01571284319207d8aa65fe9e490f09ee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 12:39:07 +0200 Subject: [PATCH 350/820] Update sentry-sdk to 1.7.0 (#74967) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 3f01b7bc5f0..44bcd8025bb 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.6.0"], + "requirements": ["sentry-sdk==1.7.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7a9c482ea84..08d9d6542ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.6.0 +sentry-sdk==1.7.0 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b7077c8ebd..a09e50bdf1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1434,7 +1434,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.6.0 +sentry-sdk==1.7.0 # homeassistant.components.sharkiq sharkiq==0.0.1 From d244d06711ebb647cd2bda97db2d4c2a0fc8d286 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 13:21:37 +0200 Subject: [PATCH 351/820] Update PyTurboJPEG to 1.6.7 (#74965) --- homeassistant/components/camera/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index aa4ca61e1d5..b1ab479f3a5 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,7 +3,7 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "dependencies": ["http"], - "requirements": ["PyTurboJPEG==1.6.6"], + "requirements": ["PyTurboJPEG==1.6.7"], "after_dependencies": ["media_player"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index e9411e53224..6c090b93ac2 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.6", "ha-av==10.0.0b4"], + "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0b4"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index 08d9d6542ca..74d53ca2c7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -44,7 +44,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.6 +PyTurboJPEG==1.6.7 # homeassistant.components.vicare PyViCare==2.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a09e50bdf1e..6069c63dcf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -40,7 +40,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.6 +PyTurboJPEG==1.6.7 # homeassistant.components.vicare PyViCare==2.16.2 From ab9950621b54ddd0d5e955a57db4724a5b222760 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 13:40:54 +0200 Subject: [PATCH 352/820] Remove toon from mypy ignore list (#74968) --- homeassistant/components/toon/__init__.py | 6 +++++- homeassistant/components/toon/config_flow.py | 4 ++-- homeassistant/components/toon/models.py | 22 ++++++++++---------- mypy.ini | 9 -------- script/hassfest/mypy_config.py | 3 --- 5 files changed, 18 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 6af3c0e066b..59174cff260 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -102,7 +102,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={ - (DOMAIN, coordinator.data.agreement.agreement_id, "meter_adapter") + ( + DOMAIN, + coordinator.data.agreement.agreement_id, + "meter_adapter", + ) # type: ignore[arg-type] }, manufacturer="Eneco", name="Meter Adapter", diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index 9afba2bc2ae..e86d951069c 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -20,8 +20,8 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): DOMAIN = DOMAIN VERSION = 2 - agreements: list[Agreement] | None = None - data: dict[str, Any] | None = None + agreements: list[Agreement] + data: dict[str, Any] @property def logger(self) -> logging.Logger: diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index e39faa1efc6..301e3c06233 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -39,8 +39,8 @@ class ToonElectricityMeterDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Electricity Meter", - identifiers={(DOMAIN, agreement_id, "electricity")}, - via_device=(DOMAIN, agreement_id, "meter_adapter"), + identifiers={(DOMAIN, agreement_id, "electricity")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "meter_adapter"), # type: ignore[typeddict-item] ) @@ -53,8 +53,8 @@ class ToonGasMeterDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Gas Meter", - identifiers={(DOMAIN, agreement_id, "gas")}, - via_device=(DOMAIN, agreement_id, "electricity"), + identifiers={(DOMAIN, agreement_id, "gas")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "electricity"), # type: ignore[typeddict-item] ) @@ -67,8 +67,8 @@ class ToonWaterMeterDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Water Meter", - identifiers={(DOMAIN, agreement_id, "water")}, - via_device=(DOMAIN, agreement_id, "electricity"), + identifiers={(DOMAIN, agreement_id, "water")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "electricity"), # type: ignore[typeddict-item] ) @@ -81,8 +81,8 @@ class ToonSolarDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Solar Panels", - identifiers={(DOMAIN, agreement_id, "solar")}, - via_device=(DOMAIN, agreement_id, "meter_adapter"), + identifiers={(DOMAIN, agreement_id, "solar")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "meter_adapter"), # type: ignore[typeddict-item] ) @@ -96,7 +96,7 @@ class ToonBoilerModuleDeviceEntity(ToonEntity): return DeviceInfo( name="Boiler Module", manufacturer="Eneco", - identifiers={(DOMAIN, agreement_id, "boiler_module")}, + identifiers={(DOMAIN, agreement_id, "boiler_module")}, # type: ignore[arg-type] via_device=(DOMAIN, agreement_id), ) @@ -110,8 +110,8 @@ class ToonBoilerDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Boiler", - identifiers={(DOMAIN, agreement_id, "boiler")}, - via_device=(DOMAIN, agreement_id, "boiler_module"), + identifiers={(DOMAIN, agreement_id, "boiler")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "boiler_module"), # type: ignore[typeddict-item] ) diff --git a/mypy.ini b/mypy.ini index e6f82ba957e..04c5b6f05e2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2801,15 +2801,6 @@ ignore_errors = true [mypy-homeassistant.components.template.sensor] ignore_errors = true -[mypy-homeassistant.components.toon] -ignore_errors = true - -[mypy-homeassistant.components.toon.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.toon.models] -ignore_errors = true - [mypy-homeassistant.components.withings] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f86fd447722..9984eeeb405 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -68,9 +68,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.statistics", "homeassistant.components.template.number", "homeassistant.components.template.sensor", - "homeassistant.components.toon", - "homeassistant.components.toon.config_flow", - "homeassistant.components.toon.models", "homeassistant.components.withings", "homeassistant.components.withings.binary_sensor", "homeassistant.components.withings.common", From ce353460b32ce89b7587109fde5adf85bb20ecaa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 14:27:54 +0200 Subject: [PATCH 353/820] Fix Withings re-authentication flow (#74961) --- homeassistant/components/withings/common.py | 37 ++++--------------- .../components/withings/config_flow.py | 22 ++++++----- .../components/withings/strings.json | 2 +- .../components/withings/translations/en.json | 2 +- tests/components/withings/common.py | 14 +++---- tests/components/withings/test_config_flow.py | 10 ++++- tests/components/withings/test_init.py | 14 ++----- 7 files changed, 38 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index ad40378eafb..90f60cd4112 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -32,7 +32,7 @@ from homeassistant.components.application_credentials import AuthImplementation from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_WEBHOOK_ID, MASS_KILOGRAMS, @@ -57,6 +57,7 @@ from . import const from .const import Measurement _LOGGER = logging.getLogger(const.LOG_NAMESPACE) +_RETRY_COEFFICIENT = 0.5 NOT_AUTHENTICATED_ERROR = re.compile( f"^{HTTPStatus.UNAUTHORIZED},.*", re.IGNORECASE, @@ -484,7 +485,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): ) -> None: """Initialize object.""" self._hass = hass - self._config_entry = config_entry + self.config_entry = config_entry self._implementation = implementation self.session = OAuth2Session(hass, config_entry, implementation) @@ -496,7 +497,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): self.session.async_ensure_token_valid(), self._hass.loop ).result() - access_token = self._config_entry.data["token"]["access_token"] + access_token = self.config_entry.data["token"]["access_token"] response = requests.request( method, f"{self.URL}/{path}", @@ -651,7 +652,7 @@ class DataManager: "Failed attempt %s of %s (%s)", attempt, attempts, exception1 ) # Make each backoff pause a little bit longer - await asyncio.sleep(0.5 * attempt) + await asyncio.sleep(_RETRY_COEFFICIENT * attempt) exception = exception1 continue @@ -738,32 +739,8 @@ class DataManager: if isinstance( exception, (UnauthorizedException, AuthFailedException) ) or NOT_AUTHENTICATED_ERROR.match(str(exception)): - context = { - const.PROFILE: self._profile, - "userid": self._user_id, - "source": SOURCE_REAUTH, - } - - # Check if reauth flow already exists. - flow = next( - iter( - flow - for flow in self._hass.config_entries.flow.async_progress_by_handler( - const.DOMAIN - ) - if flow.context == context - ), - None, - ) - if flow: - return - - # Start a reauth flow. - await self._hass.config_entries.flow.async_init( - const.DOMAIN, - context=context, - ) - return + self._api.config_entry.async_start_reauth(self._hass) + return None raise exception diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index a4ac6597248..b0fa1876d92 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -8,7 +8,6 @@ from typing import Any import voluptuous as vol from withings_api.common import AuthScope -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util import slugify @@ -25,6 +24,7 @@ class WithingsFlowHandler( # Temporarily holds authorization data during the profile step. _current_data: dict[str, None | str | int] = {} + _reauth_profile: str | None = None @property def logger(self) -> logging.Logger: @@ -53,12 +53,7 @@ class WithingsFlowHandler( async def async_step_profile(self, data: dict[str, Any]) -> FlowResult: """Prompt the user to select a user profile.""" errors = {} - reauth_profile = ( - self.context.get(const.PROFILE) - if self.context.get("source") == SOURCE_REAUTH - else None - ) - profile = data.get(const.PROFILE) or reauth_profile + profile = data.get(const.PROFILE) or self._reauth_profile if profile: existing_entries = [ @@ -67,7 +62,7 @@ class WithingsFlowHandler( if slugify(config_entry.data.get(const.PROFILE)) == slugify(profile) ] - if reauth_profile or not existing_entries: + if self._reauth_profile or not existing_entries: new_data = {**self._current_data, **data, const.PROFILE: profile} self._current_data = {} return await self.async_step_finish(new_data) @@ -81,16 +76,23 @@ class WithingsFlowHandler( ) async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Prompt user to re-authenticate.""" + self._reauth_profile = data.get(const.PROFILE) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, data: dict[str, Any] | None = None + ) -> FlowResult: """Prompt user to re-authenticate.""" if data is not None: return await self.async_step_user() - placeholders = {const.PROFILE: self.context["profile"]} + placeholders = {const.PROFILE: self._reauth_profile} self.context.update({"title_placeholders": placeholders}) return self.async_show_form( - step_id="reauth", + step_id="reauth_confirm", description_placeholders=placeholders, ) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 433888d9ebf..8f8a32c95e7 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -10,7 +10,7 @@ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, - "reauth": { + "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data." } diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json index e8acc8c3440..ca969626510 100644 --- a/homeassistant/components/withings/translations/en.json +++ b/homeassistant/components/withings/translations/en.json @@ -24,7 +24,7 @@ "description": "Provide a unique profile name for this data. Typically this is the name of the profile you selected in the previous step.", "title": "User Profile." }, - "reauth": { + "reauth_confirm": { "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", "title": "Reauthenticate Integration" } diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 66e7d6b7055..f3855ae96f0 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -42,6 +42,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import AUTH_CALLBACK_PATH from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -167,6 +168,10 @@ class ComponentFactory: ) api_mock: ConfigEntryWithingsApi = MagicMock(spec=ConfigEntryWithingsApi) + api_mock.config_entry = MockConfigEntry( + domain=const.DOMAIN, + data={"profile": profile_config.profile}, + ) ComponentFactory._setup_api_method( api_mock.user_get_device, profile_config.api_response_user_get_device ) @@ -301,15 +306,6 @@ def get_config_entries_for_user_id( ) -def async_get_flow_for_user_id(hass: HomeAssistant, user_id: int) -> list[dict]: - """Get a flow for a user id.""" - return [ - flow - for flow in hass.config_entries.flow.async_progress() - if flow["handler"] == const.DOMAIN and flow["context"].get("userid") == user_id - ] - - def get_data_manager_by_user_id( hass: HomeAssistant, user_id: int ) -> DataManager | None: diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 2643ac18c24..9fcc84dbe83 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -62,11 +62,17 @@ async def test_config_reauth_profile( result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": config_entries.SOURCE_REAUTH, "profile": "person0"}, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "title_placeholders": {"name": config_entry.title}, + "unique_id": config_entry.unique_id, + }, + data={"profile": "person0"}, ) assert result assert result["type"] == "form" - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {const.PROFILE: "person0"} result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index db26f7328ce..83e8a622e78 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -20,12 +20,7 @@ from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_setup_component -from .common import ( - ComponentFactory, - async_get_flow_for_user_id, - get_data_manager_by_user_id, - new_profile_config, -) +from .common import ComponentFactory, get_data_manager_by_user_id, new_profile_config from tests.common import MockConfigEntry @@ -122,6 +117,7 @@ async def test_async_setup_no_config(hass: HomeAssistant) -> None: [Exception("401, this is the message")], ], ) +@patch("homeassistant.components.withings.common._RETRY_COEFFICIENT", 0) async def test_auth_failure( hass: HomeAssistant, component_factory: ComponentFactory, @@ -138,20 +134,18 @@ async def test_auth_failure( ) await component_factory.configure_component(profile_configs=(person0,)) - assert not async_get_flow_for_user_id(hass, person0.user_id) + assert not hass.config_entries.flow.async_progress() await component_factory.setup_profile(person0.user_id) data_manager = get_data_manager_by_user_id(hass, person0.user_id) await data_manager.poll_data_update_coordinator.async_refresh() - flows = async_get_flow_for_user_id(hass, person0.user_id) + flows = hass.config_entries.flow.async_progress() assert flows assert len(flows) == 1 flow = flows[0] assert flow["handler"] == const.DOMAIN - assert flow["context"]["profile"] == person0.profile - assert flow["context"]["userid"] == person0.user_id result = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={} From 66e27945ac9a03ccb9c36bb32b0dec81f79de2e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 14:38:08 +0200 Subject: [PATCH 354/820] Fix mix of aiohttp and requests in ClickSend TTS (#74985) --- homeassistant/components/clicksend_tts/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 712787c34e6..8026c8e150b 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -3,7 +3,6 @@ from http import HTTPStatus import json import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -20,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) BASE_API_URL = "https://rest.clicksend.com/v3" -HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} +HEADERS = {"Content-Type": CONTENT_TYPE_JSON} CONF_LANGUAGE = "language" CONF_VOICE = "voice" From 9d2c2139037800308c1a988f29be218829f41008 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 14:49:36 +0200 Subject: [PATCH 355/820] Support overriding unit of temperature number entities (#74977) --- homeassistant/components/number/__init__.py | 40 ++++- tests/components/number/test_init.py | 159 ++++++++++++++++++++ 2 files changed, 197 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index fe438ea6aea..1820e28bc4c 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -14,8 +14,13 @@ import voluptuous as vol from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_MODE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.const import ( + ATTR_MODE, + CONF_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -69,6 +74,10 @@ UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { NumberDeviceClass.TEMPERATURE: temperature_util.convert, } +VALID_UNITS: dict[str, tuple[str, ...]] = { + NumberDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, +} + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" @@ -193,6 +202,7 @@ class NumberEntity(Entity): _attr_native_value: float _attr_native_unit_of_measurement: str | None _deprecated_number_entity_reported = False + _number_option_unit_of_measurement: str | None = None def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" @@ -226,6 +236,13 @@ class NumberEntity(Entity): report_issue, ) + async def async_internal_added_to_hass(self) -> None: + """Call when the number entity is added to hass.""" + await super().async_internal_added_to_hass() + if not self.registry_entry: + return + self.async_registry_entry_updated() + @property def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" @@ -348,6 +365,9 @@ class NumberEntity(Entity): @final def unit_of_measurement(self) -> str | None: """Return the unit of measurement of the entity, after unit conversion.""" + if self._number_option_unit_of_measurement: + return self._number_option_unit_of_measurement + if hasattr(self, "_attr_unit_of_measurement"): return self._attr_unit_of_measurement if ( @@ -467,6 +487,22 @@ class NumberEntity(Entity): report_issue, ) + @callback + def async_registry_entry_updated(self) -> None: + """Run when the entity registry entry has been updated.""" + assert self.registry_entry + if ( + (number_options := self.registry_entry.options.get(DOMAIN)) + and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT)) + and (device_class := self.device_class) in UNIT_CONVERSIONS + and self.native_unit_of_measurement in VALID_UNITS[device_class] + and custom_unit in VALID_UNITS[device_class] + ): + self._number_option_unit_of_measurement = custom_unit + return + + self._number_option_unit_of_measurement = None + @dataclasses.dataclass class NumberExtraStoredData(ExtraStoredData): diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 9921d2a639e..8d7f8a91ae8 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -22,6 +22,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM @@ -689,3 +690,161 @@ async def test_restore_number_restore_state( assert entity0.native_value == native_value assert type(entity0.native_value) == native_value_type assert entity0.native_unit_of_measurement == uom + + +@pytest.mark.parametrize( + "device_class,native_unit,custom_unit,state_unit,native_value,custom_value", + [ + # Not a supported temperature unit + ( + NumberDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + "my_temperature_unit", + TEMP_CELSIUS, + 1000, + 1000, + ), + ( + NumberDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + 37.5, + 99.5, + ), + ( + NumberDeviceClass.TEMPERATURE, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + TEMP_CELSIUS, + 100, + 38.0, + ), + ], +) +async def test_custom_unit( + hass, + enable_custom_integrations, + device_class, + native_unit, + custom_unit, + state_unit, + native_value, + custom_value, +): + """Test custom unit.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("number", "test", "very_unique") + entity_registry.async_update_entity_options( + entry.entity_id, "number", {"unit_of_measurement": custom_unit} + ) + await hass.async_block_till_done() + + platform = getattr(hass.components, "test.number") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockNumberEntity( + name="Test", + native_value=native_value, + native_unit_of_measurement=native_unit, + device_class=device_class, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(custom_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit + + +@pytest.mark.parametrize( + "native_unit, custom_unit, used_custom_unit, default_unit, native_value, custom_value, default_value", + [ + ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + 37.5, + 99.5, + 37.5, + ), + ( + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + 100, + 100, + 38.0, + ), + # Not a supported temperature unit + (TEMP_CELSIUS, "no_unit", TEMP_CELSIUS, TEMP_CELSIUS, 1000, 1000, 1000), + ], +) +async def test_custom_unit_change( + hass, + enable_custom_integrations, + native_unit, + custom_unit, + used_custom_unit, + default_unit, + native_value, + custom_value, + default_value, +): + """Test custom unit changes are picked up.""" + entity_registry = er.async_get(hass) + platform = getattr(hass.components, "test.number") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockNumberEntity( + name="Test", + native_value=native_value, + native_unit_of_measurement=native_unit, + device_class=NumberDeviceClass.TEMPERATURE, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + # Default unit conversion according to unit system + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(default_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == default_unit + + entity_registry.async_update_entity_options( + "number.test", "number", {"unit_of_measurement": custom_unit} + ) + await hass.async_block_till_done() + + # Unit conversion to the custom unit + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(custom_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == used_custom_unit + + entity_registry.async_update_entity_options( + "number.test", "number", {"unit_of_measurement": native_unit} + ) + await hass.async_block_till_done() + + # Unit conversion to another custom unit + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(native_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + entity_registry.async_update_entity_options("number.test", "number", None) + await hass.async_block_till_done() + + # Default unit conversion according to unit system + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(default_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == default_unit From 5f4713a200cc145d7d408c4f842e12a4613dc02e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 15:07:54 +0200 Subject: [PATCH 356/820] Remove solaredge from mypy ignore list (#74983) --- .../components/solaredge/config_flow.py | 2 +- homeassistant/components/solaredge/const.py | 1 + .../components/solaredge/coordinator.py | 24 +++++++++---------- homeassistant/components/solaredge/models.py | 13 +++++++--- homeassistant/components/solaredge/sensor.py | 19 +++++++++------ mypy.ini | 9 ------- script/hassfest/mypy_config.py | 3 --- 7 files changed, 36 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 58e5577ccfa..8bdf6a4b4aa 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -23,7 +23,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._errors = {} + self._errors: dict[str, str] = {} @callback def _async_current_site_ids(self) -> set[str]: diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 916a01de9e9..fd989fc8a2a 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -75,6 +75,7 @@ SENSOR_TYPES = [ ), SolarEdgeSensorEntityDescription( key="site_details", + json_key="status", name="Site details", entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index fe8f2f86a8e..7d3e949fc10 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import abstractmethod from datetime import date, datetime, timedelta +from typing import Any from solaredge import Solaredge from stringcase import snakecase @@ -23,16 +24,17 @@ from .const import ( class SolarEdgeDataService: """Get and update the latest data.""" + coordinator: DataUpdateCoordinator + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: """Initialize the data object.""" self.api = api self.site_id = site_id - self.data = {} - self.attributes = {} + self.data: dict[str, Any] = {} + self.attributes: dict[str, Any] = {} self.hass = hass - self.coordinator = None @callback def async_setup(self) -> None: @@ -105,12 +107,6 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): class SolarEdgeDetailsDataService(SolarEdgeDataService): """Get and update the latest details data.""" - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: - """Initialize the details data service.""" - super().__init__(hass, api, site_id) - - self.data = None - @property def update_interval(self) -> timedelta: """Update interval.""" @@ -125,7 +121,7 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): except KeyError as ex: raise UpdateFailed("Missing details data, skipping update") from ex - self.data = None + self.data = {} self.attributes = {} for key, value in details.items(): @@ -143,9 +139,13 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): ]: self.attributes[key] = value elif key == "status": - self.data = value + self.data["status"] = value - LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes) + LOGGER.debug( + "Updated SolarEdge details: %s, %s", + self.data.get("status"), + self.attributes, + ) class SolarEdgeInventoryDataService(SolarEdgeDataService): diff --git a/homeassistant/components/solaredge/models.py b/homeassistant/components/solaredge/models.py index ce24d854aac..57efb88023c 100644 --- a/homeassistant/components/solaredge/models.py +++ b/homeassistant/components/solaredge/models.py @@ -7,7 +7,14 @@ from homeassistant.components.sensor import SensorEntityDescription @dataclass -class SolarEdgeSensorEntityDescription(SensorEntityDescription): - """Sensor entity description for SolarEdge.""" +class SolarEdgeSensorEntityRequiredKeyMixin: + """Sensor entity description with json_key for SolarEdge.""" - json_key: str | None = None + json_key: str + + +@dataclass +class SolarEdgeSensorEntityDescription( + SensorEntityDescription, SolarEdgeSensorEntityRequiredKeyMixin +): + """Sensor entity description for SolarEdge.""" diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index a769a043442..a27c180e0b9 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -101,7 +101,7 @@ class SolarEdgeSensorFactory: def create_sensor( self, sensor_type: SolarEdgeSensorEntityDescription - ) -> SolarEdgeSensorEntityDescription: + ) -> SolarEdgeSensorEntity: """Create and return a sensor based on the sensor_key.""" sensor_class, service = self.services[sensor_type.key] @@ -155,7 +155,7 @@ class SolarEdgeDetailsSensor(SolarEdgeSensorEntity): @property def native_value(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data + return self.data_service.data.get(self.entity_description.json_key) @property def unique_id(self) -> str | None: @@ -169,7 +169,7 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API inventory sensor.""" @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self.data_service.attributes.get(self.entity_description.json_key) @@ -182,14 +182,19 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API power flow sensor.""" - def __init__(self, platform_name, sensor_type, data_service): + def __init__( + self, + platform_name: str, + sensor_type: SolarEdgeSensorEntityDescription, + data_service: SolarEdgeEnergyDetailsService, + ) -> None: """Initialize the power flow sensor.""" super().__init__(platform_name, sensor_type, data_service) self._attr_native_unit_of_measurement = data_service.unit @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self.data_service.attributes.get(self.entity_description.json_key) @@ -208,7 +213,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): self, platform_name: str, description: SolarEdgeSensorEntityDescription, - data_service: SolarEdgeDataService, + data_service: SolarEdgePowerFlowDataService, ) -> None: """Initialize the power flow sensor.""" super().__init__(platform_name, description, data_service) @@ -216,7 +221,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): self._attr_native_unit_of_measurement = data_service.unit @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self.data_service.attributes.get(self.entity_description.json_key) diff --git a/mypy.ini b/mypy.ini index 04c5b6f05e2..4975e893a38 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2750,15 +2750,6 @@ ignore_errors = true [mypy-homeassistant.components.profiler] ignore_errors = true -[mypy-homeassistant.components.solaredge.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.solaredge.coordinator] -ignore_errors = true - -[mypy-homeassistant.components.solaredge.sensor] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 9984eeeb405..d9a63aeb11f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -51,9 +51,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.sensor", "homeassistant.components.plex.media_player", "homeassistant.components.profiler", - "homeassistant.components.solaredge.config_flow", - "homeassistant.components.solaredge.coordinator", - "homeassistant.components.solaredge.sensor", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 8820ce0bddad2276840b4e3b3477b9fc5bf2ec4d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Jul 2022 15:59:46 +0200 Subject: [PATCH 357/820] Migrate NextDNS to new entity naming style (#74951) * Use new entity naming style * Remove unnecessary code * Only the first word should be capitalised --- homeassistant/components/nextdns/button.py | 9 ++-- homeassistant/components/nextdns/sensor.py | 49 +++++++++++----------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/nextdns/button.py b/homeassistant/components/nextdns/button.py index efe742a6113..1f761e6955c 100644 --- a/homeassistant/components/nextdns/button.py +++ b/homeassistant/components/nextdns/button.py @@ -1,8 +1,6 @@ """Support for the NextDNS service.""" from __future__ import annotations -from typing import cast - from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -17,7 +15,7 @@ PARALLEL_UPDATES = 1 CLEAR_LOGS_BUTTON = ButtonEntityDescription( key="clear_logs", - name="{profile_name} Clear Logs", + name="Clear logs", entity_category=EntityCategory.CONFIG, ) @@ -39,6 +37,8 @@ async def async_setup_entry( class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEntity): """Define an NextDNS button.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NextDnsStatusUpdateCoordinator, @@ -48,9 +48,6 @@ class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEnt super().__init__(coordinator) self._attr_device_info = coordinator.device_info self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" - self._attr_name = cast(str, description.name).format( - profile_name=coordinator.profile_name - ) self.entity_description = description async def async_press(self) -> None: diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 54d10ba66b9..174357864b3 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Generic, cast +from typing import Generic from nextdns import ( AnalyticsDnssec, @@ -61,7 +61,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", - name="{profile_name} DNS Queries", + name="DNS queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.all_queries, @@ -71,7 +71,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", - name="{profile_name} DNS Queries Blocked", + name="DNS queries blocked", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.blocked_queries, @@ -81,7 +81,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", - name="{profile_name} DNS Queries Relayed", + name="DNS queries relayed", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.relayed_queries, @@ -91,7 +91,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", - name="{profile_name} DNS Queries Blocked Ratio", + name="DNS queries blocked ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.blocked_queries_ratio, @@ -102,7 +102,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} DNS-over-HTTPS Queries", + name="DNS-over-HTTPS queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries, @@ -113,7 +113,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} DNS-over-TLS Queries", + name="DNS-over-TLS queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.dot_queries, @@ -124,7 +124,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} DNS-over-QUIC Queries", + name="DNS-over-QUIC queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries, @@ -135,7 +135,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} UDP Queries", + name="UDP queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.udp_queries, @@ -146,7 +146,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, icon="mdi:dns", entity_category=EntityCategory.DIAGNOSTIC, - name="{profile_name} DNS-over-HTTPS Queries Ratio", + name="DNS-over-HTTPS queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries_ratio, @@ -157,7 +157,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} DNS-over-TLS Queries Ratio", + name="DNS-over-TLS queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.dot_queries_ratio, @@ -168,7 +168,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, icon="mdi:dns", entity_category=EntityCategory.DIAGNOSTIC, - name="{profile_name} DNS-over-QUIC Queries Ratio", + name="DNS-over-QUIC queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries_ratio, @@ -179,7 +179,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} UDP Queries Ratio", + name="UDP queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.udp_queries_ratio, @@ -190,7 +190,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock", - name="{profile_name} Encrypted Queries", + name="Encrypted queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.encrypted_queries, @@ -201,7 +201,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock-open", - name="{profile_name} Unencrypted Queries", + name="Unencrypted queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.unencrypted_queries, @@ -212,7 +212,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock", - name="{profile_name} Encrypted Queries Ratio", + name="Encrypted queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.encrypted_queries_ratio, @@ -223,7 +223,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:ip", - name="{profile_name} IPv4 Queries", + name="IPv4 queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv4_queries, @@ -234,7 +234,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:ip", - name="{profile_name} IPv6 Queries", + name="IPv6 queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv6_queries, @@ -245,7 +245,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:ip", - name="{profile_name} IPv6 Queries Ratio", + name="IPv6 queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv6_queries_ratio, @@ -256,7 +256,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock-check", - name="{profile_name} DNSSEC Validated Queries", + name="DNSSEC validated queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.validated_queries, @@ -267,7 +267,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock-alert", - name="{profile_name} DNSSEC Not Validated Queries", + name="DNSSEC not validated queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.not_validated_queries, @@ -278,7 +278,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock-check", - name="{profile_name} DNSSEC Validated Queries Ratio", + name="DNSSEC validated queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.validated_queries_ratio, @@ -308,6 +308,8 @@ class NextDnsSensor( ): """Define an NextDNS sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NextDnsUpdateCoordinator[TCoordinatorData], @@ -317,9 +319,6 @@ class NextDnsSensor( super().__init__(coordinator) self._attr_device_info = coordinator.device_info self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" - self._attr_name = cast(str, description.name).format( - profile_name=coordinator.profile_name - ) self._attr_native_value = description.value(coordinator.data) self.entity_description: NextDnsSensorEntityDescription = description From c1a4dc2f229179243acd00c2231e3070e92db2af Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Jul 2022 16:00:13 +0200 Subject: [PATCH 358/820] Add NextDNS switch platform (#74512) * Add switch platform * Use lambda to get state * Use async with timeout * Add tests * Use correct type * Use Generic for coordinator * Use TCoordinatorData * Cleanup generic * Simplify coordinator data update methods * Use new entity naming style * Remove unnecessary code * Only the first word should be capitalised * Suggested change * improve typing in tests * Improve typing intests * Update tests/components/nextdns/__init__.py * black Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/nextdns/__init__.py | 62 ++-- homeassistant/components/nextdns/const.py | 2 + homeassistant/components/nextdns/switch.py | 247 +++++++++++++++ tests/components/nextdns/__init__.py | 34 ++- tests/components/nextdns/test_switch.py | 306 +++++++++++++++++++ 5 files changed, 619 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/nextdns/switch.py create mode 100644 tests/components/nextdns/test_switch.py diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 7e7f5ff2dd8..2f68abee847 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -17,6 +17,7 @@ from nextdns import ( ApiError, InvalidApiKeyError, NextDns, + Settings, ) from nextdns.model import NextDnsData @@ -34,10 +35,12 @@ from .const import ( ATTR_ENCRYPTION, ATTR_IP_VERSIONS, ATTR_PROTOCOLS, + ATTR_SETTINGS, ATTR_STATUS, CONF_PROFILE_ID, DOMAIN, UPDATE_INTERVAL_ANALYTICS, + UPDATE_INTERVAL_SETTINGS, ) TCoordinatorData = TypeVar("TCoordinatorData", bound=NextDnsData) @@ -68,6 +71,14 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) async def _async_update_data(self) -> TCoordinatorData: + """Update data via internal method.""" + try: + async with timeout(10): + return await self._async_update_data_internal() + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + async def _async_update_data_internal(self) -> TCoordinatorData: """Update data via library.""" raise NotImplementedError("Update method not implemented") @@ -75,71 +86,60 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsStatus]): """Class to manage fetching NextDNS analytics status data from API.""" - async def _async_update_data(self) -> AnalyticsStatus: + async def _async_update_data_internal(self) -> AnalyticsStatus: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_status(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_status(self.profile_id) class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsDnssec]): """Class to manage fetching NextDNS analytics Dnssec data from API.""" - async def _async_update_data(self) -> AnalyticsDnssec: + async def _async_update_data_internal(self) -> AnalyticsDnssec: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_dnssec(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_dnssec(self.profile_id) class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsEncryption]): """Class to manage fetching NextDNS analytics encryption data from API.""" - async def _async_update_data(self) -> AnalyticsEncryption: + async def _async_update_data_internal(self) -> AnalyticsEncryption: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_encryption(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_encryption(self.profile_id) class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsIpVersions]): """Class to manage fetching NextDNS analytics IP versions data from API.""" - async def _async_update_data(self) -> AnalyticsIpVersions: + async def _async_update_data_internal(self) -> AnalyticsIpVersions: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_ip_versions(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_ip_versions(self.profile_id) class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsProtocols]): """Class to manage fetching NextDNS analytics protocols data from API.""" - async def _async_update_data(self) -> AnalyticsProtocols: + async def _async_update_data_internal(self) -> AnalyticsProtocols: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_protocols(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_protocols(self.profile_id) + + +class NextDnsSettingsUpdateCoordinator(NextDnsUpdateCoordinator[Settings]): + """Class to manage fetching NextDNS connection data from API.""" + + async def _async_update_data_internal(self) -> Settings: + """Update data via library.""" + return await self.nextdns.get_settings(self.profile_id) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BUTTON, Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] COORDINATORS = [ (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_IP_VERSIONS, NextDnsIpVersionsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_PROTOCOLS, NextDnsProtocolsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_SETTINGS, NextDnsSettingsUpdateCoordinator, UPDATE_INTERVAL_SETTINGS), (ATTR_STATUS, NextDnsStatusUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), ] diff --git a/homeassistant/components/nextdns/const.py b/homeassistant/components/nextdns/const.py index 04bab44354b..d455dd79635 100644 --- a/homeassistant/components/nextdns/const.py +++ b/homeassistant/components/nextdns/const.py @@ -5,11 +5,13 @@ ATTR_DNSSEC = "dnssec" ATTR_ENCRYPTION = "encryption" ATTR_IP_VERSIONS = "ip_versions" ATTR_PROTOCOLS = "protocols" +ATTR_SETTINGS = "settings" ATTR_STATUS = "status" CONF_PROFILE_ID = "profile_id" CONF_PROFILE_NAME = "profile_name" UPDATE_INTERVAL_ANALYTICS = timedelta(minutes=10) +UPDATE_INTERVAL_SETTINGS = timedelta(minutes=1) DOMAIN = "nextdns" diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py new file mode 100644 index 00000000000..4bd3c14c20f --- /dev/null +++ b/homeassistant/components/nextdns/switch.py @@ -0,0 +1,247 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Generic + +from nextdns import Settings + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsSettingsUpdateCoordinator, TCoordinatorData +from .const import ATTR_SETTINGS, DOMAIN + +PARALLEL_UPDATES = 1 + + +@dataclass +class NextDnsSwitchRequiredKeysMixin(Generic[TCoordinatorData]): + """Class for NextDNS entity required keys.""" + + state: Callable[[TCoordinatorData], bool] + + +@dataclass +class NextDnsSwitchEntityDescription( + SwitchEntityDescription, NextDnsSwitchRequiredKeysMixin[TCoordinatorData] +): + """NextDNS switch entity description.""" + + +SWITCHES = ( + NextDnsSwitchEntityDescription[Settings]( + key="block_page", + name="Block page", + entity_category=EntityCategory.CONFIG, + icon="mdi:web-cancel", + state=lambda data: data.block_page, + ), + NextDnsSwitchEntityDescription[Settings]( + key="cache_boost", + name="Cache boost", + entity_category=EntityCategory.CONFIG, + icon="mdi:memory", + state=lambda data: data.cache_boost, + ), + NextDnsSwitchEntityDescription[Settings]( + key="cname_flattening", + name="CNAME flattening", + entity_category=EntityCategory.CONFIG, + icon="mdi:tournament", + state=lambda data: data.cname_flattening, + ), + NextDnsSwitchEntityDescription[Settings]( + key="anonymized_ecs", + name="Anonymized EDNS client subnet", + entity_category=EntityCategory.CONFIG, + icon="mdi:incognito", + state=lambda data: data.anonymized_ecs, + ), + NextDnsSwitchEntityDescription[Settings]( + key="logs", + name="Logs", + entity_category=EntityCategory.CONFIG, + icon="mdi:file-document-outline", + state=lambda data: data.logs, + ), + NextDnsSwitchEntityDescription[Settings]( + key="web3", + name="Web3", + entity_category=EntityCategory.CONFIG, + icon="mdi:web", + state=lambda data: data.web3, + ), + NextDnsSwitchEntityDescription[Settings]( + key="allow_affiliate", + name="Allow affiliate & tracking links", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.allow_affiliate, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_disguised_trackers", + name="Block disguised third-party trackers", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_disguised_trackers, + ), + NextDnsSwitchEntityDescription[Settings]( + key="ai_threat_detection", + name="AI-Driven threat detection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.ai_threat_detection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_csam", + name="Block child sexual abuse material", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_csam, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_ddns", + name="Block dynamic DNS hostnames", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_ddns, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_nrd", + name="Block newly registered domains", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_nrd, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_parked_domains", + name="Block parked domains", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_parked_domains, + ), + NextDnsSwitchEntityDescription[Settings]( + key="cryptojacking_protection", + name="Cryptojacking protection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.cryptojacking_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="dga_protection", + name="Domain generation algorithms protection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.dga_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="dns_rebinding_protection", + name="DNS rebinding protection", + entity_category=EntityCategory.CONFIG, + icon="mdi:dns", + state=lambda data: data.dns_rebinding_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="google_safe_browsing", + name="Google safe browsing", + entity_category=EntityCategory.CONFIG, + icon="mdi:google", + state=lambda data: data.google_safe_browsing, + ), + NextDnsSwitchEntityDescription[Settings]( + key="idn_homograph_attacks_protection", + name="IDN homograph attacks protection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.idn_homograph_attacks_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="threat_intelligence_feeds", + name="Threat intelligence feeds", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.threat_intelligence_feeds, + ), + NextDnsSwitchEntityDescription[Settings]( + key="typosquatting_protection", + name="Typosquatting protection", + entity_category=EntityCategory.CONFIG, + icon="mdi:keyboard-outline", + state=lambda data: data.typosquatting_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_bypass_methods", + name="Block bypass methods", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_bypass_methods, + ), + NextDnsSwitchEntityDescription[Settings]( + key="safesearch", + name="Force SafeSearch", + entity_category=EntityCategory.CONFIG, + icon="mdi:search-web", + state=lambda data: data.safesearch, + ), + NextDnsSwitchEntityDescription[Settings]( + key="youtube_restricted_mode", + name="Force YouTube restricted mode", + entity_category=EntityCategory.CONFIG, + icon="mdi:youtube", + state=lambda data: data.youtube_restricted_mode, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add NextDNS entities from a config_entry.""" + coordinator: NextDnsSettingsUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + ATTR_SETTINGS + ] + + switches: list[NextDnsSwitch] = [] + for description in SWITCHES: + switches.append(NextDnsSwitch(coordinator, description)) + + async_add_entities(switches) + + +class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchEntity): + """Define an NextDNS switch.""" + + _attr_has_entity_name = True + entity_description: NextDnsSwitchEntityDescription + + def __init__( + self, + coordinator: NextDnsSettingsUpdateCoordinator, + description: NextDnsSwitchEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_is_on = description.state(coordinator.data) + self.entity_description = description + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = self.entity_description.state(self.coordinator.data) + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on switch.""" + result = await self.coordinator.nextdns.set_setting( + self.coordinator.profile_id, self.entity_description.key, True + ) + + if result: + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off switch.""" + result = await self.coordinator.nextdns.set_setting( + self.coordinator.profile_id, self.entity_description.key, False + ) + + if result: + self._attr_is_on = False + self.async_write_ha_state() diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 24063d957d4..82c55f56bbb 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -7,10 +7,12 @@ from nextdns import ( AnalyticsIpVersions, AnalyticsProtocols, AnalyticsStatus, + Settings, ) from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -24,9 +26,36 @@ IP_VERSIONS = AnalyticsIpVersions(ipv4_queries=90, ipv6_queries=10) PROTOCOLS = AnalyticsProtocols( doh_queries=20, doq_queries=10, dot_queries=30, udp_queries=40 ) +SETTINGS = Settings( + ai_threat_detection=True, + allow_affiliate=True, + anonymized_ecs=True, + block_bypass_methods=True, + block_csam=True, + block_ddns=True, + block_disguised_trackers=True, + block_nrd=True, + block_page=False, + block_parked_domains=True, + cache_boost=True, + cname_flattening=True, + cryptojacking_protection=True, + dga_protection=True, + dns_rebinding_protection=True, + google_safe_browsing=False, + idn_homograph_attacks_protection=True, + logs=True, + safesearch=False, + threat_intelligence_feeds=True, + typosquatting_protection=True, + web3=True, + youtube_restricted_mode=False, +) -async def init_integration(hass, add_to_hass=True) -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, add_to_hass: bool = True +) -> MockConfigEntry: """Set up the NextDNS integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -55,6 +84,9 @@ async def init_integration(hass, add_to_hass=True) -> MockConfigEntry: ), patch( "homeassistant.components.nextdns.NextDns.get_analytics_protocols", return_value=PROTOCOLS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_settings", + return_value=SETTINGS, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py new file mode 100644 index 00000000000..3e07a2633d1 --- /dev/null +++ b/tests/components/nextdns/test_switch.py @@ -0,0 +1,306 @@ +"""Test switch of NextDNS integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import SETTINGS, init_integration + +from tests.common import async_fire_time_changed + + +async def test_switch(hass: HomeAssistant) -> None: + """Test states of the switches.""" + registry = er.async_get(hass) + + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_ai_driven_threat_detection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_ai_driven_threat_detection") + assert entry + assert entry.unique_id == "xyz12_ai_threat_detection" + + state = hass.states.get("switch.fake_profile_allow_affiliate_tracking_links") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_allow_affiliate_tracking_links") + assert entry + assert entry.unique_id == "xyz12_allow_affiliate" + + state = hass.states.get("switch.fake_profile_anonymized_edns_client_subnet") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_anonymized_edns_client_subnet") + assert entry + assert entry.unique_id == "xyz12_anonymized_ecs" + + state = hass.states.get("switch.fake_profile_block_bypass_methods") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_bypass_methods") + assert entry + assert entry.unique_id == "xyz12_block_bypass_methods" + + state = hass.states.get("switch.fake_profile_block_child_sexual_abuse_material") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_child_sexual_abuse_material") + assert entry + assert entry.unique_id == "xyz12_block_csam" + + state = hass.states.get("switch.fake_profile_block_disguised_third_party_trackers") + assert state + assert state.state == STATE_ON + + entry = registry.async_get( + "switch.fake_profile_block_disguised_third_party_trackers" + ) + assert entry + assert entry.unique_id == "xyz12_block_disguised_trackers" + + state = hass.states.get("switch.fake_profile_block_dynamic_dns_hostnames") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_dynamic_dns_hostnames") + assert entry + assert entry.unique_id == "xyz12_block_ddns" + + state = hass.states.get("switch.fake_profile_block_newly_registered_domains") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_newly_registered_domains") + assert entry + assert entry.unique_id == "xyz12_block_nrd" + + state = hass.states.get("switch.fake_profile_block_page") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_block_page") + assert entry + assert entry.unique_id == "xyz12_block_page" + + state = hass.states.get("switch.fake_profile_block_parked_domains") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_parked_domains") + assert entry + assert entry.unique_id == "xyz12_block_parked_domains" + + state = hass.states.get("switch.fake_profile_cname_flattening") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_cname_flattening") + assert entry + assert entry.unique_id == "xyz12_cname_flattening" + + state = hass.states.get("switch.fake_profile_cache_boost") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_cache_boost") + assert entry + assert entry.unique_id == "xyz12_cache_boost" + + state = hass.states.get("switch.fake_profile_cryptojacking_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_cryptojacking_protection") + assert entry + assert entry.unique_id == "xyz12_cryptojacking_protection" + + state = hass.states.get("switch.fake_profile_dns_rebinding_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_dns_rebinding_protection") + assert entry + assert entry.unique_id == "xyz12_dns_rebinding_protection" + + state = hass.states.get( + "switch.fake_profile_domain_generation_algorithms_protection" + ) + assert state + assert state.state == STATE_ON + + entry = registry.async_get( + "switch.fake_profile_domain_generation_algorithms_protection" + ) + assert entry + assert entry.unique_id == "xyz12_dga_protection" + + state = hass.states.get("switch.fake_profile_force_safesearch") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_force_safesearch") + assert entry + assert entry.unique_id == "xyz12_safesearch" + + state = hass.states.get("switch.fake_profile_force_youtube_restricted_mode") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_force_youtube_restricted_mode") + assert entry + assert entry.unique_id == "xyz12_youtube_restricted_mode" + + state = hass.states.get("switch.fake_profile_google_safe_browsing") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_google_safe_browsing") + assert entry + assert entry.unique_id == "xyz12_google_safe_browsing" + + state = hass.states.get("switch.fake_profile_idn_homograph_attacks_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_idn_homograph_attacks_protection") + assert entry + assert entry.unique_id == "xyz12_idn_homograph_attacks_protection" + + state = hass.states.get("switch.fake_profile_logs") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_logs") + assert entry + assert entry.unique_id == "xyz12_logs" + + state = hass.states.get("switch.fake_profile_threat_intelligence_feeds") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_threat_intelligence_feeds") + assert entry + assert entry.unique_id == "xyz12_threat_intelligence_feeds" + + state = hass.states.get("switch.fake_profile_typosquatting_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_typosquatting_protection") + assert entry + assert entry.unique_id == "xyz12_typosquatting_protection" + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_web3") + assert entry + assert entry.unique_id == "xyz12_web3" + + +async def test_switch_on(hass: HomeAssistant) -> None: + """Test the switch can be turned on.""" + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_block_page") + assert state + assert state.state == STATE_OFF + + with patch( + "homeassistant.components.nextdns.NextDns.set_setting", return_value=True + ) as mock_switch_on: + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.fake_profile_block_page"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_block_page") + assert state + assert state.state == STATE_ON + + mock_switch_on.assert_called_once() + + +async def test_switch_off(hass: HomeAssistant) -> None: + """Test the switch can be turned on.""" + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_ON + + with patch( + "homeassistant.components.nextdns.NextDns.set_setting", return_value=True + ) as mock_switch_on: + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.fake_profile_web3"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_OFF + + mock_switch_on.assert_called_once() + + +async def test_availability(hass: HomeAssistant) -> None: + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON + + future = utcnow() + timedelta(minutes=10) + with patch( + "homeassistant.components.nextdns.NextDns.get_settings", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=20) + with patch( + "homeassistant.components.nextdns.NextDns.get_settings", + return_value=SETTINGS, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON From ee4749b207d45b06ef1d5be7127327684389434f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 11 Jul 2022 16:21:26 +0200 Subject: [PATCH 359/820] Change more properties to attributes for rfxtrx (#74880) * Move more properties to attributes * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Convert states to attributes * Adjustments from review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/rfxtrx/__init__.py | 30 +++++------ .../components/rfxtrx/binary_sensor.py | 36 +++++-------- homeassistant/components/rfxtrx/cover.py | 53 ++++++++----------- homeassistant/components/rfxtrx/light.py | 36 +++++-------- homeassistant/components/rfxtrx/sensor.py | 17 ++---- homeassistant/components/rfxtrx/switch.py | 21 +++----- 6 files changed, 76 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index efb9b634d62..3257a482c0c 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -440,6 +440,14 @@ def get_device_tuple_from_identifiers( return DeviceTuple(identifier2[1], identifier2[2], identifier2[3]) +def get_identifiers_from_device_tuple( + device_tuple: DeviceTuple, +) -> set[tuple[str, str]]: + """Calculate the device identifier from a device tuple.""" + # work around legacy identifier, being a multi tuple value + return {(DOMAIN, *device_tuple)} # type: ignore[arg-type] + + async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: @@ -469,10 +477,15 @@ class RfxtrxEntity(RestoreEntity): event: rfxtrxmod.RFXtrxEvent | None = None, ) -> None: """Initialize the device.""" + self._attr_device_info = DeviceInfo( + identifiers=get_identifiers_from_device_tuple(device_id), + model=device.type_string, + name=f"{device.type_string} {device.id_string}", + ) + self._attr_unique_id = "_".join(x for x in device_id) self._device = device self._event = event self._device_id = device_id - self._unique_id = "_".join(x for x in self._device_id) # If id_string is 213c7f2:1, the group_id is 213c7f2, and the device will respond to # group events regardless of their group indices. (self._group_id, _, _) = device.id_string.partition(":") @@ -493,20 +506,6 @@ class RfxtrxEntity(RestoreEntity): return None return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - @property - def unique_id(self): - """Return unique identifier of remote device.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - return DeviceInfo( - identifiers={(DOMAIN, *self._device_id)}, - model=self._device.type_string, - name=f"{self._device.type_string} {self._device.id_string}", - ) - def _event_applies(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): """Check if event applies to me.""" if isinstance(event, rfxtrxmod.ControlEvent): @@ -546,7 +545,6 @@ class RfxtrxCommandEntity(RfxtrxEntity): ) -> None: """Initialzie a switch or light device.""" super().__init__(device, device_id, event=event) - self._state: bool | None = None async def _async_send(self, fun, *args): rfx_object = self.hass.data[DOMAIN][DATA_RFXOBJECT] diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index ef315c7b974..1a0fe698dcf 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -124,6 +124,9 @@ async def async_setup_entry( class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """A representation of a RFXtrx binary sensor.""" + _attr_force_update = True + """We should force updates. Repeated states have meaning.""" + def __init__( self, device: rfxtrxmod.RFXtrxDevice, @@ -140,7 +143,6 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self.entity_description = entity_description self._data_bits = data_bits self._off_delay = off_delay - self._state: bool | None = None self._delay_listener: CALLBACK_TYPE | None = None self._cmd_on = cmd_on self._cmd_off = cmd_off @@ -152,20 +154,10 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_ON + self._attr_is_on = old_state.state == STATE_ON - if self._state and self._off_delay is not None: - self._state = False - - @property - def force_update(self) -> bool: - """We should force updates. Repeated states have meaning.""" - return True - - @property - def is_on(self): - """Return true if the sensor state is True.""" - return self._state + if self.is_on and self._off_delay is not None: + self._attr_is_on = False def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): """Apply event for a lighting 4 device.""" @@ -174,22 +166,22 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): assert cmdstr cmd = int(cmdstr, 16) if cmd == self._cmd_on: - self._state = True + self._attr_is_on = True elif cmd == self._cmd_off: - self._state = False + self._attr_is_on = False else: - self._state = True + self._attr_is_on = True def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent): assert isinstance(event, (rfxtrxmod.SensorEvent, rfxtrxmod.ControlEvent)) if event.values.get("Command") in COMMAND_ON_LIST: - self._state = True + self._attr_is_on = True elif event.values.get("Command") in COMMAND_OFF_LIST: - self._state = False + self._attr_is_on = False elif event.values.get("Sensor Status") in SENSOR_STATUS_ON: - self._state = True + self._attr_is_on = True elif event.values.get("Sensor Status") in SENSOR_STATUS_OFF: - self._state = False + self._attr_is_on = False def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): """Apply command from rfxtrx.""" @@ -226,7 +218,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): def off_delay_listener(now): """Switch device off after a delay.""" self._delay_listener = None - self._state = False + self._attr_is_on = False self.async_write_ha_state() self._delay_listener = evt.async_call_later( diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 6bf49beb89a..5e1788194f5 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -71,6 +71,21 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): """Initialize the RFXtrx cover device.""" super().__init__(device, device_id, event) self._venetian_blind_mode = venetian_blind_mode + self._attr_is_closed: bool | None = True + + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP + ) + + if venetian_blind_mode in ( + CONST_VENETIAN_BLIND_MODE_US, + CONST_VENETIAN_BLIND_MODE_EU, + ): + self._attr_supported_features |= ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + ) async def async_added_to_hass(self) -> None: """Restore device state.""" @@ -79,31 +94,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_OPEN - - @property - def supported_features(self) -> int: - """Flag supported features.""" - supported_features = ( - CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP - ) - - if self._venetian_blind_mode in ( - CONST_VENETIAN_BLIND_MODE_US, - CONST_VENETIAN_BLIND_MODE_EU, - ): - supported_features |= ( - CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.STOP_TILT - ) - - return supported_features - - @property - def is_closed(self) -> bool: - """Return if the cover is closed.""" - return not self._state + self._attr_is_closed = old_state.state != STATE_OPEN async def async_open_cover(self, **kwargs: Any) -> None: """Move the cover up.""" @@ -113,7 +104,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): await self._async_send(self._device.send_up2sec) else: await self._async_send(self._device.send_open) - self._state = True + self._attr_is_closed = False self.async_write_ha_state() async def async_close_cover(self, **kwargs: Any) -> None: @@ -124,13 +115,13 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): await self._async_send(self._device.send_down2sec) else: await self._async_send(self._device.send_close) - self._state = False + self._attr_is_closed = True self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._async_send(self._device.send_stop) - self._state = True + self._attr_is_closed = False self.async_write_ha_state() async def async_open_cover_tilt(self, **kwargs: Any) -> None: @@ -150,7 +141,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" await self._async_send(self._device.send_stop) - self._state = True + self._attr_is_closed = False self.async_write_ha_state() def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): @@ -158,9 +149,9 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: - self._state = True + self._attr_is_closed = False elif event.values["Command"] in COMMAND_OFF_LIST: - self._state = False + self._attr_is_closed = True @callback def _handle_event(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 87f8c68aaac..198d0ffd3f1 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -56,7 +56,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - _brightness = 0 + _attr_brightness: int = 0 _device: rfxtrxmod.LightingDevice async def async_added_to_hass(self): @@ -66,37 +66,28 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_ON - self._brightness = old_state.attributes.get(ATTR_BRIGHTNESS) - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def is_on(self): - """Return true if device is on.""" - return self._state + self._attr_is_on = old_state.state == STATE_ON + if brightness := old_state.attributes.get(ATTR_BRIGHTNESS): + self._attr_brightness = int(brightness) async def async_turn_on(self, **kwargs): """Turn the device on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) - self._state = True + self._attr_is_on = True if brightness is None: await self._async_send(self._device.send_on) - self._brightness = 255 + self._attr_brightness = 255 else: await self._async_send(self._device.send_dim, brightness * 100 // 255) - self._brightness = brightness + self._attr_brightness = brightness self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" await self._async_send(self._device.send_off) - self._state = False - self._brightness = 0 + self._attr_is_on = False + self._attr_brightness = 0 self.async_write_ha_state() def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): @@ -104,12 +95,13 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: - self._state = True + self._attr_is_on = True elif event.values["Command"] in COMMAND_OFF_LIST: - self._state = False + self._attr_is_on = False elif event.values["Command"] == "Set level": - self._brightness = event.values["Dim level"] * 255 // 100 - self._state = self._brightness > 0 + brightness = event.values["Dim level"] * 255 // 100 + self._attr_brightness = brightness + self._attr_is_on = brightness > 0 @callback def _handle_event(self, event, device_id): diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 6ff3073b900..86c3eabc922 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -273,15 +273,16 @@ async def async_setup_entry( class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Representation of a RFXtrx sensor.""" + _attr_force_update = True + """We should force updates. Repeated states have meaning.""" + entity_description: RfxtrxSensorEntityDescription def __init__(self, device, device_id, entity_description, event=None): """Initialize the sensor.""" super().__init__(device, device_id, event=event) self.entity_description = entity_description - self._unique_id = "_".join( - x for x in (*self._device_id, entity_description.key) - ) + self._attr_unique_id = "_".join(x for x in (*device_id, entity_description.key)) async def async_added_to_hass(self): """Restore device state.""" @@ -302,16 +303,6 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): value = self._event.values.get(self.entity_description.key) return self.entity_description.convert(value) - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def force_update(self) -> bool: - """We should force updates. Repeated states have meaning.""" - return True - @callback def _handle_event(self, event, device_id): """Check if event applies to me and update.""" diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index fc826b4ebe6..f164e54b212 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -94,7 +94,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_ON + self._attr_is_on = old_state.state == STATE_ON def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): """Apply event for a lighting 4 device.""" @@ -103,18 +103,18 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): assert cmdstr cmd = int(cmdstr, 16) if cmd == self._cmd_on: - self._state = True + self._attr_is_on = True elif cmd == self._cmd_off: - self._state = False + self._attr_is_on = False else: - self._state = True + self._attr_is_on = True def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent) -> None: assert isinstance(event, rfxtrxmod.ControlEvent) if event.values["Command"] in COMMAND_ON_LIST: - self._state = True + self._attr_is_on = True elif event.values["Command"] in COMMAND_OFF_LIST: - self._state = False + self._attr_is_on = False def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" @@ -134,18 +134,13 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self.async_write_ha_state() - @property - def is_on(self): - """Return true if device is on.""" - return self._state - async def async_turn_on(self, **kwargs): """Turn the device on.""" if self._cmd_on is not None: await self._async_send(self._device.send_command, self._cmd_on) else: await self._async_send(self._device.send_on) - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): @@ -154,5 +149,5 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): await self._async_send(self._device.send_command, self._cmd_off) else: await self._async_send(self._device.send_off) - self._state = False + self._attr_is_on = False self.async_write_ha_state() From 06a4c226fdc180e59e66d0d4fe33894e7d6c7802 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 16:44:44 +0200 Subject: [PATCH 360/820] Remove konnected from mypy ignore list (#75003) --- homeassistant/components/konnected/__init__.py | 2 +- homeassistant/components/konnected/config_flow.py | 12 +++++++----- mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 98440766334..620ed12ac54 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -419,7 +419,7 @@ class KonnectedView(HomeAssistantView): resp = {} if request.query.get(CONF_ZONE): resp[CONF_ZONE] = zone_num - else: + elif zone_num: resp[CONF_PIN] = ZONE_TO_PIN[zone_num] # Make sure entity is setup diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 94a58227c56..fcf94a38c18 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -6,6 +6,7 @@ import copy import logging import random import string +from typing import Any from urllib.parse import urlparse import voluptuous as vol @@ -171,11 +172,11 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 # class variable to store/share discovered host information - discovered_hosts = {} + discovered_hosts: dict[str, dict[str, Any]] = {} def __init__(self) -> None: """Initialize the Konnected flow.""" - self.data = {} + self.data: dict[str, Any] = {} self.options = OPTIONS_SCHEMA({CONF_IO: {}}) async def async_gen_config(self, host, port): @@ -271,6 +272,7 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Malformed Konnected SSDP info") else: # extract host/port from ssdp_location + assert discovery_info.ssdp_location netloc = urlparse(discovery_info.ssdp_location).netloc.split(":") self._async_abort_entries_match( {CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])} @@ -392,10 +394,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.current_opt = self.entry.options or self.entry.data[CONF_DEFAULT_OPTIONS] # as config proceeds we'll build up new options and then replace what's in the config entry - self.new_opt = {CONF_IO: {}} + self.new_opt: dict[str, dict[str, Any]] = {CONF_IO: {}} self.active_cfg = None - self.io_cfg = {} - self.current_states = [] + self.io_cfg: dict[str, Any] = {} + self.current_states: list[dict[str, Any]] = [] self.current_state = 1 @callback diff --git a/mypy.ini b/mypy.ini index 4975e893a38..ca7fd4a15c2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2690,12 +2690,6 @@ ignore_errors = true [mypy-homeassistant.components.izone.climate] ignore_errors = true -[mypy-homeassistant.components.konnected] -ignore_errors = true - -[mypy-homeassistant.components.konnected.config_flow] -ignore_errors = true - [mypy-homeassistant.components.lovelace] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d9a63aeb11f..21309560eb3 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -31,8 +31,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.icloud.device_tracker", "homeassistant.components.icloud.sensor", "homeassistant.components.izone.climate", - "homeassistant.components.konnected", - "homeassistant.components.konnected.config_flow", "homeassistant.components.lovelace", "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", From fe1c23321e615fee486e1a1a527c20e8cd4ca0a3 Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Mon, 11 Jul 2022 16:51:10 +0200 Subject: [PATCH 361/820] Update pyialarm to 2.2.0 (#74874) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 60ecb9da74a..00a0cba9a27 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.9.0"], + "requirements": ["pyialarm==2.2.0"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 74d53ca2c7b..5376d91ed2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1556,7 +1556,7 @@ pyhomematic==0.1.77 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.9.0 +pyialarm==2.2.0 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6069c63dcf2..8ad2dd3957f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1053,7 +1053,7 @@ pyhiveapi==0.5.13 pyhomematic==0.1.77 # homeassistant.components.ialarm -pyialarm==1.9.0 +pyialarm==2.2.0 # homeassistant.components.icloud pyicloud==1.0.0 From 73a8ae35c296f4d2cfb9c1b0773f8f672826993f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 16:53:14 +0200 Subject: [PATCH 362/820] Remove izone from mypy ignore list (#75005) --- homeassistant/components/izone/climate.py | 26 +++++++++++------------ mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 419e1709b45..a26490e78c8 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -76,7 +76,7 @@ async def async_setup_entry( @callback def init_controller(ctrl: Controller): """Register the controller device and the containing zones.""" - conf: ConfigType = hass.data.get(DATA_CONFIG) + conf: ConfigType | None = hass.data.get(DATA_CONFIG) # Filter out any entities excluded in the config file if conf and ctrl.device_uid in conf[CONF_EXCLUDE]: @@ -107,8 +107,6 @@ async def async_setup_entry( "async_set_airflow_max", ) - return True - def _return_on_connection_error(ret=None): def wrap(func): @@ -310,7 +308,7 @@ class ControllerDevice(ClimateEntity): return key assert False, "Should be unreachable" - @property + @property # type: ignore[misc] @_return_on_connection_error([]) def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" @@ -318,13 +316,13 @@ class ControllerDevice(ClimateEntity): return [HVACMode.OFF, HVACMode.FAN_ONLY] return [HVACMode.OFF, *self._state_to_pizone] - @property + @property # type: ignore[misc] @_return_on_connection_error(PRESET_NONE) def preset_mode(self): """Eco mode is external air.""" return PRESET_ECO if self._controller.free_air else PRESET_NONE - @property + @property # type: ignore[misc] @_return_on_connection_error([PRESET_NONE]) def preset_modes(self): """Available preset modes, normal or eco.""" @@ -332,7 +330,7 @@ class ControllerDevice(ClimateEntity): return [PRESET_NONE, PRESET_ECO] return [PRESET_NONE] - @property + @property # type: ignore[misc] @_return_on_connection_error() def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -362,7 +360,7 @@ class ControllerDevice(ClimateEntity): return None return zone.target_temperature - @property + @property # type: ignore[misc] @_return_on_connection_error() def target_temperature(self) -> float | None: """Return the temperature we try to reach (either from control zone or master unit).""" @@ -390,13 +388,13 @@ class ControllerDevice(ClimateEntity): """Return the list of available fan modes.""" return list(self._fan_to_pizone) - @property + @property # type: ignore[misc] @_return_on_connection_error(0.0) def min_temp(self) -> float: """Return the minimum temperature.""" return self._controller.temp_min - @property + @property # type: ignore[misc] @_return_on_connection_error(50.0) def max_temp(self) -> float: """Return the maximum temperature.""" @@ -471,7 +469,9 @@ class ZoneDevice(ClimateEntity): self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE self._attr_device_info = DeviceInfo( - identifiers={(IZONE, controller.unique_id, zone.index)}, + identifiers={ + (IZONE, controller.unique_id, zone.index) # type:ignore[arg-type] + }, manufacturer="IZone", model=zone.type.name.title(), name=self.name, @@ -533,7 +533,7 @@ class ZoneDevice(ClimateEntity): """ return False - @property + @property # type: ignore[misc] @_return_on_connection_error(0) def supported_features(self): """Return the list of supported features.""" @@ -552,7 +552,7 @@ class ZoneDevice(ClimateEntity): return PRECISION_TENTHS @property - def hvac_mode(self) -> HVACMode: + def hvac_mode(self) -> HVACMode | None: """Return current operation ie. heat, cool, idle.""" mode = self._zone.mode for (key, value) in self._state_to_pizone.items(): diff --git a/mypy.ini b/mypy.ini index ca7fd4a15c2..ce02b366ad0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2687,9 +2687,6 @@ ignore_errors = true [mypy-homeassistant.components.icloud.sensor] ignore_errors = true -[mypy-homeassistant.components.izone.climate] -ignore_errors = true - [mypy-homeassistant.components.lovelace] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 21309560eb3..8d36184290b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -30,7 +30,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", "homeassistant.components.icloud.sensor", - "homeassistant.components.izone.climate", "homeassistant.components.lovelace", "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", From af2feb3d40f6cc6801477caab3419dc98afce443 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 16:54:01 +0200 Subject: [PATCH 363/820] Update pyupgrade to v2.37.1 (#74989) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- tests/components/august/mocks.py | 3 ++- tests/components/calendar/test_trigger.py | 4 ++-- tests/components/google/conftest.py | 4 ++-- tests/components/nest/common.py | 4 ++-- tests/components/stream/conftest.py | 2 +- tests/components/unifiprotect/utils.py | 3 ++- 8 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91bda72e616..bbf7295be99 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.1 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index caed3668db7..b1a9a0a1a5b 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.34.0 +pyupgrade==2.37.1 yamllint==1.27.1 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index c93e6429f1c..932065f37da 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -1,10 +1,11 @@ """Mocks for the august component.""" from __future__ import annotations +from collections.abc import Iterable import json import os import time -from typing import Any, Iterable +from typing import Any from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from yalexs.activity import ( diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index a3b5bacca59..ebe5e9185e4 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -8,11 +8,11 @@ forward exercising the triggers. """ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Generator import datetime import logging import secrets -from typing import Any, Generator +from typing import Any from unittest.mock import patch import pytest diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index f47d1232582..7e668de2891 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,10 +1,10 @@ """Test configuration and mocks for the google integration.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator import datetime import http -from typing import Any, Generator, TypeVar +from typing import Any, TypeVar from unittest.mock import Mock, mock_open, patch from aiohttp.client_exceptions import ClientError diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index f86112ada75..fbdc2665879 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator import copy from dataclasses import dataclass, field import time -from typing import Any, Generator, TypeVar +from typing import Any, TypeVar from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.device import Device diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 91b4106c1f4..ff1a66a1215 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -12,10 +12,10 @@ so that it can inspect the output. from __future__ import annotations import asyncio +from collections.abc import Generator from http import HTTPStatus import logging import threading -from typing import Generator from unittest.mock import Mock, patch from aiohttp import web diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 260c6996128..8d54cbddea6 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -2,9 +2,10 @@ # pylint: disable=protected-access from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass from datetime import timedelta -from typing import Any, Callable, Sequence +from typing import Any, Callable from unittest.mock import Mock from pyunifiprotect import ProtectApiClient From eb922b2a1f6d8bb3e6f5701602042102e401a9fa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 16:55:06 +0200 Subject: [PATCH 364/820] Add temperature number to demo integration (#74986) --- homeassistant/components/demo/number.py | 54 ++++++++++++++++--------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 02ab1a2a989..17382ab9962 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -1,9 +1,9 @@ """Demo platform that offers a fake Number entity.""" from __future__ import annotations -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.const import DEVICE_DEFAULT_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -35,10 +35,10 @@ async def async_setup_platform( 0.42, "mdi:square-wave", False, - 0.0, - 1.0, - 0.01, - NumberMode.BOX, + native_min_value=0.0, + native_max_value=1.0, + native_step=0.01, + mode=NumberMode.BOX, ), DemoNumber( "large_range", @@ -46,9 +46,9 @@ async def async_setup_platform( 500, "mdi:square-wave", False, - 1, - 1000, - 1, + native_min_value=1, + native_max_value=1000, + native_step=1, ), DemoNumber( "small_range", @@ -56,9 +56,22 @@ async def async_setup_platform( 128, "mdi:square-wave", False, - 1, - 255, - 1, + native_min_value=1, + native_max_value=255, + native_step=1, + ), + DemoNumber( + "temp1", + "Temperature setting", + 22, + "mdi:thermometer", + False, + device_class=NumberDeviceClass.TEMPERATURE, + native_min_value=15.0, + native_max_value=35.0, + native_step=1, + mode=NumberMode.BOX, + unit_of_measurement=TEMP_CELSIUS, ), ] ) @@ -84,19 +97,24 @@ class DemoNumber(NumberEntity): name: str, state: float, icon: str, - assumed: bool, + assumed_state: bool, + *, + device_class: NumberDeviceClass | None = None, + mode: NumberMode = NumberMode.AUTO, native_min_value: float | None = None, native_max_value: float | None = None, native_step: float | None = None, - mode: NumberMode = NumberMode.AUTO, + unit_of_measurement: str | None = None, ) -> None: """Initialize the Demo Number entity.""" - self._attr_assumed_state = assumed + self._attr_assumed_state = assumed_state + self._attr_device_class = device_class self._attr_icon = icon - self._attr_name = name or DEVICE_DEFAULT_NAME - self._attr_unique_id = unique_id - self._attr_native_value = state self._attr_mode = mode + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_native_value = state + self._attr_unique_id = unique_id if native_min_value is not None: self._attr_native_min_value = native_min_value From c2fefe03b2dc800f42de695f0b73a8f26621d882 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jul 2022 17:14:00 +0200 Subject: [PATCH 365/820] Add support for subscribing to bluetooth callbacks by address (#74773) --- .../components/bluetooth/__init__.py | 119 +++++++- homeassistant/components/bluetooth/models.py | 50 +++- tests/components/bluetooth/test_init.py | 268 +++++++++++++++++- 3 files changed, 415 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index cf7f1884869..26c1af3fb92 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,7 +8,7 @@ import fnmatch from functools import cached_property import logging import platform -from typing import Final +from typing import Final, TypedDict from bleak import BleakError from bleak.backends.device import MANUFACTURERS, BLEDevice @@ -26,7 +26,11 @@ from homeassistant.core import ( from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import BluetoothMatcher, async_get_bluetooth +from homeassistant.loader import ( + BluetoothMatcher, + BluetoothMatcherOptional, + async_get_bluetooth, +) from . import models from .const import DOMAIN @@ -38,6 +42,19 @@ _LOGGER = logging.getLogger(__name__) MAX_REMEMBER_ADDRESSES: Final = 2048 +class BluetoothCallbackMatcherOptional(TypedDict, total=False): + """Matcher for the bluetooth integration for callback optional fields.""" + + address: str + + +class BluetoothCallbackMatcher( + BluetoothMatcherOptional, + BluetoothCallbackMatcherOptional, +): + """Callback matcher for the bluetooth integration.""" + + class BluetoothScanningMode(Enum): """The mode of scanning for bluetooth devices.""" @@ -50,6 +67,7 @@ SCANNING_MODE_TO_BLEAK = { BluetoothScanningMode.PASSIVE: "passive", } +ADDRESS: Final = "address" LOCAL_NAME: Final = "local_name" SERVICE_UUID: Final = "service_uuid" MANUFACTURER_ID: Final = "manufacturer_id" @@ -102,11 +120,34 @@ BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[[BluetoothServiceInfo, BluetoothChange], None] +@hass_callback +def async_discovered_service_info( + hass: HomeAssistant, +) -> list[BluetoothServiceInfo]: + """Return the discovered devices list.""" + if DOMAIN not in hass.data: + return [] + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_discovered_service_info() + + +@hass_callback +def async_address_present( + hass: HomeAssistant, + address: str, +) -> bool: + """Check if an address is present in the bluetooth device list.""" + if DOMAIN not in hass.data: + return False + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_address_present(address) + + @hass_callback def async_register_callback( hass: HomeAssistant, callback: BluetoothCallback, - match_dict: BluetoothMatcher | None, + match_dict: BluetoothCallbackMatcher | None, ) -> Callable[[], None]: """Register to receive a callback on bluetooth change. @@ -128,9 +169,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def _ble_device_matches( - matcher: BluetoothMatcher, device: BLEDevice, advertisement_data: AdvertisementData + matcher: BluetoothCallbackMatcher | BluetoothMatcher, + device: BLEDevice, + advertisement_data: AdvertisementData, ) -> bool: """Check if a ble device and advertisement_data matches the matcher.""" + if ( + matcher_address := matcher.get(ADDRESS) + ) is not None and device.address != matcher_address: + return False + if ( matcher_local_name := matcher.get(LOCAL_NAME) ) is not None and not fnmatch.fnmatch( @@ -192,7 +240,9 @@ class BluetoothManager: self._integration_matchers = integration_matchers self.scanner: HaBleakScanner | None = None self._cancel_device_detected: CALLBACK_TYPE | None = None - self._callbacks: list[tuple[BluetoothCallback, BluetoothMatcher | None]] = [] + self._callbacks: list[ + tuple[BluetoothCallback, BluetoothCallbackMatcher | None] + ] = [] # Some devices use a random address so we need to use # an LRU to avoid memory issues. self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES) @@ -227,14 +277,22 @@ class BluetoothManager: ) -> None: """Handle a detected device.""" matched_domains: set[str] | None = None - if device.address not in self._matched: + match_key = (device.address, bool(advertisement_data.manufacturer_data)) + match_key_has_mfr_data = (device.address, True) + + # If we matched without manufacturer_data, we need to do it again + # since we may think the device is unsupported otherwise + if ( + match_key_has_mfr_data not in self._matched + and match_key not in self._matched + ): matched_domains = { matcher["domain"] for matcher in self._integration_matchers if _ble_device_matches(matcher, device, advertisement_data) } if matched_domains: - self._matched[device.address] = True + self._matched[match_key] = True _LOGGER.debug( "Device detected: %s with advertisement_data: %s matched domains: %s", device, @@ -275,18 +333,61 @@ class BluetoothManager: @hass_callback def async_register_callback( - self, callback: BluetoothCallback, match_dict: BluetoothMatcher | None = None + self, + callback: BluetoothCallback, + matcher: BluetoothCallbackMatcher | None = None, ) -> Callable[[], None]: """Register a callback.""" - callback_entry = (callback, match_dict) + callback_entry = (callback, matcher) self._callbacks.append(callback_entry) @hass_callback def _async_remove_callback() -> None: self._callbacks.remove(callback_entry) + # If we have history for the subscriber, we can trigger the callback + # immediately with the last packet so the subscriber can see the + # device. + if ( + matcher + and (address := matcher.get(ADDRESS)) + and models.HA_BLEAK_SCANNER + and (device_adv_data := models.HA_BLEAK_SCANNER.history.get(address)) + ): + try: + callback( + BluetoothServiceInfo.from_advertisement(*device_adv_data), + BluetoothChange.ADVERTISEMENT, + ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + return _async_remove_callback + @hass_callback + def async_address_present(self, address: str) -> bool: + """Return if the address is present.""" + return bool( + models.HA_BLEAK_SCANNER + and any( + device.address == address + for device in models.HA_BLEAK_SCANNER.discovered_devices + ) + ) + + @hass_callback + def async_discovered_service_info(self) -> list[BluetoothServiceInfo]: + """Return if the address is present.""" + if models.HA_BLEAK_SCANNER: + discovered = models.HA_BLEAK_SCANNER.discovered_devices + history = models.HA_BLEAK_SCANNER.history + return [ + BluetoothServiceInfo.from_advertisement(*history[device.address]) + for device in discovered + if device.address in history + ] + return [] + async def async_stop(self, event: Event) -> None: """Stop bluetooth discovery.""" if self._cancel_device_detected: diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index a2651c587f7..43d4d0cb923 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -8,7 +8,11 @@ from typing import Any, Final, cast from bleak import BleakScanner from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData, AdvertisementDataCallback +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BaseBleakScanner, +) from lru import LRU # pylint: disable=no-name-in-module from homeassistant.core import CALLBACK_TYPE, callback as hass_callback @@ -52,7 +56,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] self._callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] - self._history: LRU = LRU(MAX_HISTORY_SIZE) + self.history: LRU = LRU(MAX_HISTORY_SIZE) super().__init__(*args, **kwargs) @hass_callback @@ -70,7 +74,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] # Replay the history since otherwise we miss devices # that were already discovered before the callback was registered # or we are in passive mode - for device, advertisement_data in self._history.values(): + for device, advertisement_data in self.history.values(): _dispatch_callback(callback, filters, device, advertisement_data) return _remove_callback @@ -83,31 +87,46 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] Here we get the actual callback from bleak and dispatch it to all the wrapped HaBleakScannerWrapper classes """ - self._history[device.address] = (device, advertisement_data) + self.history[device.address] = (device, advertisement_data) for callback_filters in self._callbacks: _dispatch_callback(*callback_filters, device, advertisement_data) -class HaBleakScannerWrapper(BleakScanner): # type: ignore[misc] +class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] """A wrapper that uses the single instance.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the BleakScanner.""" self._detection_cancel: CALLBACK_TYPE | None = None self._mapped_filters: dict[str, set[str]] = {} - if "filters" in kwargs: - self._mapped_filters = {k: set(v) for k, v in kwargs["filters"].items()} - if "service_uuids" in kwargs: - self._mapped_filters[FILTER_UUIDS] = set(kwargs["service_uuids"]) + self._adv_data_callback: AdvertisementDataCallback | None = None + self._map_filters(*args, **kwargs) super().__init__(*args, **kwargs) async def stop(self, *args: Any, **kwargs: Any) -> None: """Stop scanning for devices.""" - return async def start(self, *args: Any, **kwargs: Any) -> None: """Start scanning for devices.""" - return + + def _map_filters(self, *args: Any, **kwargs: Any) -> bool: + """Map the filters.""" + mapped_filters = {} + if filters := kwargs.get("filters"): + if FILTER_UUIDS not in filters: + _LOGGER.warning("Only %s filters are supported", FILTER_UUIDS) + mapped_filters = {k: set(v) for k, v in filters.items()} + if service_uuids := kwargs.get("service_uuids"): + mapped_filters[FILTER_UUIDS] = set(service_uuids) + if mapped_filters == self._mapped_filters: + return False + self._mapped_filters = mapped_filters + return True + + def set_scanning_filter(self, *args: Any, **kwargs: Any) -> None: + """Set the filters to use.""" + if self._map_filters(*args, **kwargs): + self._setup_detection_callback() def _cancel_callback(self) -> None: """Cancel callback.""" @@ -127,8 +146,15 @@ class HaBleakScannerWrapper(BleakScanner): # type: ignore[misc] This method takes the callback and registers it with the long running scanner. """ + self._adv_data_callback = callback + self._setup_detection_callback() + + def _setup_detection_callback(self) -> None: + """Set up the detection callback.""" + if self._adv_data_callback is None: + return self._cancel_callback() - super().register_detection_callback(callback) + super().register_detection_callback(self._adv_data_callback) assert HA_BLEAK_SCANNER is not None self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( self._callback, self._mapped_filters diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 1e0647df01c..f43ef4737f6 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -78,6 +78,30 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): assert "Could not create bluetooth scanner" in caplog.text +async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): + """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" + mock_bt = [] + with patch( + "homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError + ) as mock_ha_bleak_scanner, patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_ha_bleak_scanner.mock_calls) == 1 + assert "Could not create bluetooth scanner" in caplog.text + assert not bluetooth.async_discovered_service_info(hass) + assert not bluetooth.async_address_present(hass, "aa:bb:bb:dd:ee:ff") + + async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): """Test bluetooth discovery match by service_uuid.""" mock_bt = [ @@ -207,8 +231,47 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( assert len(mock_config_flow.mock_calls) == 0 +async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): + """Test the async_discovered_device_api.""" + mock_bt = [] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + assert not bluetooth.async_discovered_service_info(hass) + assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") + + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + assert not bluetooth.async_discovered_service_info(hass) + + wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + service_infos = bluetooth.async_discovered_service_info(hass) + assert len(service_infos) == 1 + # wrong_name should not appear because bleak no longer sees it + assert service_infos[0].name == "wohand" + + assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False + assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True + + async def test_register_callbacks(hass, mock_bleak_scanner_start): - """Test configured options for a device are loaded via config entry.""" + """Test registering a callback.""" mock_bt = [] callbacks = [] @@ -284,6 +347,92 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): assert service_info.manufacturer_id is None +async def test_register_callback_by_address(hass, mock_bleak_scanner_start): + """Test registering a callback by address.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + if len(callbacks) >= 3: + raise ValueError + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + # 3rd callback raises ValueError but is still tracked + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + # 4th callback should not be tracked since we canceled + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + # Now register again with a callback that fails to + # make sure we do not perm fail + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + ) + cancel() + + # Now register again, since the 3rd callback + # should fail but we should still record it + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + ) + cancel() + + assert len(callbacks) == 3 + + for idx in range(3): + service_info: BluetoothServiceInfo = callbacks[idx][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + async def test_wrapped_instance_with_filter(hass, mock_bleak_scanner_start): """Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner.""" with patch( @@ -438,3 +587,120 @@ async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_s models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 1 + + +async def test_wrapped_instance_changes_uuids(hass, mock_bleak_scanner_start): + """Test consumers can use the wrapped instance can change the uuids later.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter(service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]) + scanner.register_detection_callback(_device_detected) + + type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] + for _ in range(2): + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + assert len(detected) == 2 + + +async def test_wrapped_instance_changes_filters(hass, mock_bleak_scanner_start): + """Test consumers can use the wrapped instance can change the filter later.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:42", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:62", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) + + type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] + for _ in range(2): + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + assert len(detected) == 2 + + +async def test_wrapped_instance_unsupported_filter( + hass, mock_bleak_scanner_start, caplog +): + """Test we want when their filter is ineffective.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={"unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + assert "Only UUIDs filters are supported" in caplog.text From fa51a39f1dcf96dadc69bd8cda5a7cffedad4560 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:24:35 +0200 Subject: [PATCH 366/820] Use instance attributes in evohome (#74996) --- homeassistant/components/evohome/__init__.py | 36 ++----------------- homeassistant/components/evohome/climate.py | 31 ++++++++-------- .../components/evohome/water_heater.py | 26 ++++++++------ mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 5 files changed, 35 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 908dff48aef..1bb84dfc40a 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, - TEMP_CELSIUS, Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -519,13 +518,14 @@ class EvoDevice(Entity): DHW controller. """ + _attr_should_poll = False + def __init__(self, evo_broker, evo_device) -> None: """Initialize the evohome entity.""" self._evo_device = evo_device self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs - self._unique_id = self._name = self._icon = self._precision = None self._device_state_attrs = {} async def async_refresh(self, payload: dict | None = None) -> None: @@ -533,7 +533,7 @@ class EvoDevice(Entity): if payload is None: self.async_schedule_update_ha_state(force_refresh=True) return - if payload["unique_id"] != self._unique_id: + if payload["unique_id"] != self._attr_unique_id: return if payload["service"] in (SVC_SET_ZONE_OVERRIDE, SVC_RESET_ZONE_OVERRIDE): await self.async_zone_svc_request(payload["service"], payload["data"]) @@ -548,21 +548,6 @@ class EvoDevice(Entity): """Process a service request (setpoint override) for a zone.""" raise NotImplementedError - @property - def should_poll(self) -> bool: - """Evohome entities should not be polled.""" - return False - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the evohome entity.""" - return self._name - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the evohome-specific state attributes.""" @@ -576,25 +561,10 @@ class EvoDevice(Entity): return {"status": convert_dict(status)} - @property - def icon(self) -> str: - """Return the icon to use in the frontend UI.""" - return self._icon - async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" async_dispatcher_connect(self.hass, DOMAIN, self.async_refresh) - @property - def precision(self) -> float: - """Return the temperature precision to use in the frontend UI.""" - return self._precision - - @property - def temperature_unit(self) -> str: - """Return the temperature unit to use in the frontend UI.""" - return TEMP_CELSIUS - class EvoChild(EvoDevice): """Base for any evohome child. diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 676bc88f470..700fa6ab078 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -13,7 +13,7 @@ from homeassistant.components.climate.const import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.const import PRECISION_TENTHS +from homeassistant.const import PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -151,19 +151,19 @@ class EvoZone(EvoChild, EvoClimateEntity): if evo_device.modelType.startswith("VisionProWifi"): # this system does not have a distinct ID for the zone - self._unique_id = f"{evo_device.zoneId}z" + self._attr_unique_id = f"{evo_device.zoneId}z" else: - self._unique_id = evo_device.zoneId + self._attr_unique_id = evo_device.zoneId - self._name = evo_device.name - self._icon = "mdi:radiator" + self._attr_name = evo_device.name if evo_broker.client_v1: - self._precision = PRECISION_TENTHS + self._attr_precision = PRECISION_TENTHS else: - self._precision = self._evo_device.setpointCapabilities["valueResolution"] + self._attr_precision = self._evo_device.setpointCapabilities[ + "valueResolution" + ] - self._preset_modes = list(HA_PRESET_TO_EVO) self._attr_supported_features = ( ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) @@ -313,22 +313,23 @@ class EvoController(EvoClimateEntity): It is assumed there is only one TCS per location, and they are thus synonymous. """ + _attr_icon = "mdi:thermostat" + _attr_precision = PRECISION_TENTHS + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Controller/Location.""" super().__init__(evo_broker, evo_device) - self._unique_id = evo_device.systemId - self._name = evo_device.location.name - self._icon = "mdi:thermostat" - - self._precision = PRECISION_TENTHS + self._attr_unique_id = evo_device.systemId + self._attr_name = evo_device.location.name modes = [m["systemMode"] for m in evo_broker.config["allowedSystemModes"]] - self._preset_modes = [ + self._attr_preset_modes = [ TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA) ] self._attr_supported_features = ( - ClimateEntityFeature.PRESET_MODE if self._preset_modes else 0 + ClimateEntityFeature.PRESET_MODE if self._attr_preset_modes else 0 ) async def async_tcs_svc_request(self, service: dict, data: dict) -> None: diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index f216862b232..ff54cfbe4a6 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -7,7 +7,13 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.const import ( + PRECISION_TENTHS, + PRECISION_WHOLE, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -51,15 +57,20 @@ async def async_setup_platform( class EvoDHW(EvoChild, WaterHeaterEntity): """Base for a Honeywell TCC DHW controller (aka boiler).""" + _attr_name = "DHW controller" + _attr_icon = "mdi:thermometer-lines" + _attr_operation_list = list(HA_STATE_TO_EVO) + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize an evohome DHW controller.""" super().__init__(evo_broker, evo_device) - self._unique_id = evo_device.dhwId - self._name = "DHW controller" - self._icon = "mdi:thermometer-lines" + self._attr_unique_id = evo_device.dhwId - self._precision = PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE + self._attr_precision = ( + PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE + ) self._attr_supported_features = ( WaterHeaterEntityFeature.AWAY_MODE | WaterHeaterEntityFeature.OPERATION_MODE ) @@ -71,11 +82,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity): return STATE_AUTO return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] - @property - def operation_list(self) -> list[str]: - """Return the list of available operations.""" - return list(HA_STATE_TO_EVO) - @property def is_away_mode_on(self): """Return True if away mode is on.""" diff --git a/mypy.ini b/mypy.ini index ce02b366ad0..8335af1ed77 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2672,9 +2672,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.climate] ignore_errors = true -[mypy-homeassistant.components.evohome.water_heater] -ignore_errors = true - [mypy-homeassistant.components.icloud] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 8d36184290b..b492a70c8a9 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -25,7 +25,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.conversation.default_agent", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", - "homeassistant.components.evohome.water_heater", "homeassistant.components.icloud", "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", From e1e85caf189193cf048f401ecca8453cb13afbd8 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Jul 2022 17:26:07 +0200 Subject: [PATCH 367/820] Migrate Nettigo Air Monitor to new entity naming style (#74993) --- homeassistant/components/nam/__init__.py | 3 +- homeassistant/components/nam/button.py | 6 +- homeassistant/components/nam/const.py | 175 -------------------- homeassistant/components/nam/sensor.py | 199 ++++++++++++++++++++++- 4 files changed, 203 insertions(+), 180 deletions(-) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 302c0af76d4..25615db6eed 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -30,7 +30,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( ATTR_SDS011, ATTR_SPS30, - DEFAULT_NAME, DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER, @@ -130,7 +129,7 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): """Return the device info.""" return DeviceInfo( connections={(CONNECTION_NETWORK_MAC, cast(str, self._unique_id))}, - name=DEFAULT_NAME, + name="Nettigo Air Monitor", sw_version=self.nam.software_version, manufacturer=MANUFACTURER, configuration_url=f"http://{self.nam.host}/", diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index db5474ec925..6725ef3292d 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NAMDataUpdateCoordinator -from .const import DEFAULT_NAME, DOMAIN +from .const import DOMAIN PARALLEL_UPDATES = 1 @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription( key="restart", - name=f"{DEFAULT_NAME} Restart", + name="Restart", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, ) @@ -44,6 +44,8 @@ async def async_setup_entry( class NAMButton(CoordinatorEntity[NAMDataUpdateCoordinator], ButtonEntity): """Define an Nettigo Air Monitor button.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NAMDataUpdateCoordinator, diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index b81e7337a9f..e7c6f2532ef 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -4,21 +4,6 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, - PERCENTAGE, - PRESSURE_HPA, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_CELSIUS, -) -from homeassistant.helpers.entity import EntityCategory - SUFFIX_P0: Final = "_p0" SUFFIX_P1: Final = "_p1" SUFFIX_P2: Final = "_p2" @@ -49,7 +34,6 @@ ATTR_SPS30_P2: Final = f"{ATTR_SPS30}{SUFFIX_P2}" ATTR_SPS30_P4: Final = f"{ATTR_SPS30}{SUFFIX_P4}" ATTR_UPTIME: Final = "uptime" -DEFAULT_NAME: Final = "Nettigo Air Monitor" DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) DOMAIN: Final = "nam" MANUFACTURER: Final = "Nettigo" @@ -58,162 +42,3 @@ MIGRATION_SENSORS: Final = [ ("temperature", ATTR_DHT22_TEMPERATURE), ("humidity", ATTR_DHT22_HUMIDITY), ] - -SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( - SensorEntityDescription( - key=ATTR_BME280_HUMIDITY, - name=f"{DEFAULT_NAME} BME280 Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BME280_PRESSURE, - name=f"{DEFAULT_NAME} BME280 Pressure", - native_unit_of_measurement=PRESSURE_HPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BME280_TEMPERATURE, - name=f"{DEFAULT_NAME} BME280 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP180_PRESSURE, - name=f"{DEFAULT_NAME} BMP180 Pressure", - native_unit_of_measurement=PRESSURE_HPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP180_TEMPERATURE, - name=f"{DEFAULT_NAME} BMP180 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP280_PRESSURE, - name=f"{DEFAULT_NAME} BMP280 Pressure", - native_unit_of_measurement=PRESSURE_HPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP280_TEMPERATURE, - name=f"{DEFAULT_NAME} BMP280 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_HECA_HUMIDITY, - name=f"{DEFAULT_NAME} HECA Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_HECA_TEMPERATURE, - name=f"{DEFAULT_NAME} HECA Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_MHZ14A_CARBON_DIOXIDE, - name=f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.CO2, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SDS011_P1, - name=f"{DEFAULT_NAME} SDS011 Particulate Matter 10", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM10, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SDS011_P2, - name=f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM25, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SHT3X_HUMIDITY, - name=f"{DEFAULT_NAME} SHT3X Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SHT3X_TEMPERATURE, - name=f"{DEFAULT_NAME} SHT3X Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P0, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM1, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P1, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 10", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM10, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P2, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM25, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P4, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:molecule", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_DHT22_HUMIDITY, - name=f"{DEFAULT_NAME} DHT22 Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_DHT22_TEMPERATURE, - name=f"{DEFAULT_NAME} DHT22 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SIGNAL_STRENGTH, - name=f"{DEFAULT_NAME} Signal Strength", - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_UPTIME, - name=f"{DEFAULT_NAME} Uptime", - device_class=SensorDeviceClass.TIMESTAMP, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), -) diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index af729cf9066..6229102035e 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -7,24 +7,219 @@ from typing import cast from homeassistant.components.sensor import ( DOMAIN as PLATFORM, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow from . import NAMDataUpdateCoordinator -from .const import ATTR_UPTIME, DOMAIN, MIGRATION_SENSORS, SENSORS +from .const import ( + ATTR_BME280_HUMIDITY, + ATTR_BME280_PRESSURE, + ATTR_BME280_TEMPERATURE, + ATTR_BMP180_PRESSURE, + ATTR_BMP180_TEMPERATURE, + ATTR_BMP280_PRESSURE, + ATTR_BMP280_TEMPERATURE, + ATTR_DHT22_HUMIDITY, + ATTR_DHT22_TEMPERATURE, + ATTR_HECA_HUMIDITY, + ATTR_HECA_TEMPERATURE, + ATTR_MHZ14A_CARBON_DIOXIDE, + ATTR_SDS011_P1, + ATTR_SDS011_P2, + ATTR_SHT3X_HUMIDITY, + ATTR_SHT3X_TEMPERATURE, + ATTR_SIGNAL_STRENGTH, + ATTR_SPS30_P0, + ATTR_SPS30_P1, + ATTR_SPS30_P2, + ATTR_SPS30_P4, + ATTR_UPTIME, + DOMAIN, + MIGRATION_SENSORS, +) PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=ATTR_BME280_HUMIDITY, + name="BME280 humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BME280_PRESSURE, + name="BME280 pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BME280_TEMPERATURE, + name="BME280 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP180_PRESSURE, + name="BMP180 pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP180_TEMPERATURE, + name="BMP180 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP280_PRESSURE, + name="BMP280 pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP280_TEMPERATURE, + name="BMP280 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_HECA_HUMIDITY, + name="HECA humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_HECA_TEMPERATURE, + name="HECA temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_MHZ14A_CARBON_DIOXIDE, + name="MH-Z14A carbon dioxide", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SDS011_P1, + name="SDS011 particulate matter 10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SDS011_P2, + name="SDS011 particulate matter 2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SHT3X_HUMIDITY, + name="SHT3X humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SHT3X_TEMPERATURE, + name="SHT3X temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P0, + name="SPS30 particulate matter 1.0", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM1, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P1, + name="SPS30 particulate matter 10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P2, + name="SPS30 particulate matter 2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P4, + name="SPS30 particulate matter 4.0", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:molecule", + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_DHT22_HUMIDITY, + name="DHT22 humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_DHT22_TEMPERATURE, + name="DHT22 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SIGNAL_STRENGTH, + name="Signal strength", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_UPTIME, + name="Uptime", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -61,6 +256,8 @@ async def async_setup_entry( class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): """Define an Nettigo Air Monitor sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NAMDataUpdateCoordinator, From 7b5cf63a4689199195b358a5d70a34dd0a4ee4dc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Jul 2022 17:30:22 +0200 Subject: [PATCH 368/820] Migrate Airly to new entity naming style (#74995) --- homeassistant/components/airly/const.py | 1 - homeassistant/components/airly/sensor.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 801bca58412..8fddaea8ec2 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -25,7 +25,6 @@ SUFFIX_LIMIT: Final = "LIMIT" ATTRIBUTION: Final = "Data provided by Airly" CONF_USE_NEAREST: Final = "use_nearest" -DEFAULT_NAME: Final = "Airly" DOMAIN: Final = "airly" LABEL_ADVICE: Final = "advice" MANUFACTURER: Final = "Airly sp. z o.o." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 9b647b93afa..eeb0037c814 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -45,7 +45,6 @@ from .const import ( ATTR_LIMIT, ATTR_PERCENT, ATTRIBUTION, - DEFAULT_NAME, DOMAIN, MANUFACTURER, SUFFIX_LIMIT, @@ -137,6 +136,7 @@ async def async_setup_entry( class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): """Define an Airly sensor.""" + _attr_has_entity_name = True entity_description: AirlySensorEntityDescription def __init__( @@ -151,12 +151,11 @@ class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")}, manufacturer=MANUFACTURER, - name=DEFAULT_NAME, + name=name, configuration_url=URL.format( latitude=coordinator.latitude, longitude=coordinator.longitude ), ) - self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{description.key}".lower() ) From 6ac05784a63f7490f875959139ef903034bc45b0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:33:20 +0200 Subject: [PATCH 369/820] Remove icloud from mypy ignore list (#75007) --- homeassistant/components/icloud/__init__.py | 16 ++++--- homeassistant/components/icloud/account.py | 45 +++++++++---------- .../components/icloud/device_tracker.py | 6 +-- homeassistant/components/icloud/sensor.py | 4 +- mypy.ini | 12 ----- script/hassfest/mypy_config.py | 4 -- 6 files changed, 37 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 06028ebce6c..63802804f4d 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,4 +1,8 @@ """The iCloud component.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -82,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=username) - icloud_dir = Store(hass, STORAGE_VERSION, STORAGE_KEY) + icloud_dir = Store[Any](hass, STORAGE_VERSION, STORAGE_KEY) account = IcloudAccount( hass, @@ -103,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def play_sound(service: ServiceCall) -> None: """Play sound on the device.""" account = service.data[ATTR_ACCOUNT] - device_name = service.data.get(ATTR_DEVICE_NAME) + device_name: str = service.data[ATTR_DEVICE_NAME] device_name = slugify(device_name.replace(" ", "", 99)) for device in _get_account(account).get_devices_with_name(device_name): @@ -112,7 +116,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def display_message(service: ServiceCall) -> None: """Display a message on the device.""" account = service.data[ATTR_ACCOUNT] - device_name = service.data.get(ATTR_DEVICE_NAME) + device_name: str = service.data[ATTR_DEVICE_NAME] device_name = slugify(device_name.replace(" ", "", 99)) message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) sound = service.data.get(ATTR_LOST_DEVICE_SOUND, False) @@ -123,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def lost_device(service: ServiceCall) -> None: """Make the device in lost state.""" account = service.data[ATTR_ACCOUNT] - device_name = service.data.get(ATTR_DEVICE_NAME) + device_name: str = service.data[ATTR_DEVICE_NAME] device_name = slugify(device_name.replace(" ", "", 99)) number = service.data.get(ATTR_LOST_DEVICE_NUMBER) message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) @@ -139,11 +143,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: _get_account(account).keep_alive() - def _get_account(account_identifier: str) -> any: + def _get_account(account_identifier: str) -> IcloudAccount: if account_identifier is None: return None - icloud_account = hass.data[DOMAIN].get(account_identifier) + icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) if icloud_account is None: for account in hass.data[DOMAIN].values(): if account.username == account_identifier: diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 95b90791165..4dc3c07aba7 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -91,21 +91,19 @@ class IcloudAccount: self._username = username self._password = password self._with_family = with_family - self._fetch_interval = max_interval + self._fetch_interval: float = max_interval self._max_interval = max_interval self._gps_accuracy_threshold = gps_accuracy_threshold self._icloud_dir = icloud_dir self.api: PyiCloudService | None = None - self._owner_fullname = None - self._family_members_fullname = {} - self._devices = {} + self._owner_fullname: str | None = None + self._family_members_fullname: dict[str, str] = {} + self._devices: dict[str, IcloudDevice] = {} self._retried_fetch = False self._config_entry = config_entry - self.listeners = [] - def setup(self) -> None: """Set up an iCloud account.""" try: @@ -271,6 +269,8 @@ class IcloudAccount: distances = [] for zone_state in zones: + if zone_state is None: + continue zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE] zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE] zone_distance = distance( @@ -279,7 +279,8 @@ class IcloudAccount: zone_state_lat, zone_state_long, ) - distances.append(round(zone_distance / 1000, 1)) + if zone_distance is not None: + distances.append(round(zone_distance / 1000, 1)) # Max interval if no zone if not distances: @@ -288,7 +289,7 @@ class IcloudAccount: # Calculate out how long it would take for the device to drive # to the nearest zone at 120 km/h: - interval = round(mindistance / 2, 0) + interval = round(mindistance / 2) # Never poll more than once per minute interval = max(interval, 1) @@ -324,7 +325,7 @@ class IcloudAccount: self.api.authenticate() self.update_devices() - def get_devices_with_name(self, name: str) -> [any]: + def get_devices_with_name(self, name: str) -> list[Any]: """Get devices by name.""" result = [] name_slug = slugify(name.replace(" ", "", 99)) @@ -341,7 +342,7 @@ class IcloudAccount: return self._username @property - def owner_fullname(self) -> str: + def owner_fullname(self) -> str | None: """Return the account owner fullname.""" return self._owner_fullname @@ -351,7 +352,7 @@ class IcloudAccount: return self._family_members_fullname @property - def fetch_interval(self) -> int: + def fetch_interval(self) -> float: """Return the account fetch interval.""" return self._fetch_interval @@ -386,14 +387,7 @@ class IcloudDevice: self._device_class = self._status[DEVICE_CLASS] self._device_model = self._status[DEVICE_DISPLAY_NAME] - if self._status[DEVICE_PERSON_ID]: - owner_fullname = account.family_members_fullname[ - self._status[DEVICE_PERSON_ID] - ] - else: - owner_fullname = account.owner_fullname - - self._battery_level = None + self._battery_level: int | None = None self._battery_status = None self._location = None @@ -402,8 +396,13 @@ class IcloudDevice: ATTR_ACCOUNT_FETCH_INTERVAL: self._account.fetch_interval, ATTR_DEVICE_NAME: self._device_model, ATTR_DEVICE_STATUS: None, - ATTR_OWNER_NAME: owner_fullname, } + if self._status[DEVICE_PERSON_ID]: + self._attrs[ATTR_OWNER_NAME] = account.family_members_fullname[ + self._status[DEVICE_PERSON_ID] + ] + elif account.owner_fullname is not None: + self._attrs[ATTR_OWNER_NAME] = account.owner_fullname def update(self, status) -> None: """Update the iCloud device.""" @@ -487,17 +486,17 @@ class IcloudDevice: return self._device_model @property - def battery_level(self) -> int: + def battery_level(self) -> int | None: """Return the Apple device battery level.""" return self._battery_level @property - def battery_status(self) -> str: + def battery_status(self) -> str | None: """Return the Apple device battery status.""" return self._battery_status @property - def location(self) -> dict[str, Any]: + def location(self) -> dict[str, Any] | None: """Return the Apple device location.""" return self._location diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index c9d251b06c7..6886d500a84 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -36,7 +36,7 @@ async def async_setup_entry( ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] - tracked = set() + tracked = set[str]() @callback def update_account(): @@ -51,7 +51,7 @@ async def async_setup_entry( @callback -def add_entities(account, async_add_entities, tracked): +def add_entities(account: IcloudAccount, async_add_entities, tracked): """Add new tracker entities from the account.""" new_tracked = [] @@ -101,7 +101,7 @@ class IcloudTrackerEntity(TrackerEntity): return self._device.location[DEVICE_LOCATION_LONGITUDE] @property - def battery_level(self) -> int: + def battery_level(self) -> int | None: """Return the battery level of the device.""" return self._device.battery_level diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 38ea3af62b6..6e415aa3350 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -21,7 +21,7 @@ async def async_setup_entry( ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] - tracked = set() + tracked = set[str]() @callback def update_account(): @@ -74,7 +74,7 @@ class IcloudDeviceBatterySensor(SensorEntity): return f"{self._device.name} battery state" @property - def native_value(self) -> int: + def native_value(self) -> int | None: """Battery state percentage.""" return self._device.battery_level diff --git a/mypy.ini b/mypy.ini index 8335af1ed77..65b51e9d1c9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2672,18 +2672,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.climate] ignore_errors = true -[mypy-homeassistant.components.icloud] -ignore_errors = true - -[mypy-homeassistant.components.icloud.account] -ignore_errors = true - -[mypy-homeassistant.components.icloud.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.icloud.sensor] -ignore_errors = true - [mypy-homeassistant.components.lovelace] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b492a70c8a9..17ef8edc529 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -25,10 +25,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.conversation.default_agent", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", - "homeassistant.components.icloud", - "homeassistant.components.icloud.account", - "homeassistant.components.icloud.device_tracker", - "homeassistant.components.icloud.sensor", "homeassistant.components.lovelace", "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", From 6fd47d035ea658aa93f28b4cd741ed7cfb399a3b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 11 Jul 2022 08:40:52 -0700 Subject: [PATCH 370/820] Add basic Rhasspy integration (#74942) Co-authored-by: Franck Nijhof --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/rhasspy/__init__.py | 15 +++++++ .../components/rhasspy/config_flow.py | 29 ++++++++++++ homeassistant/components/rhasspy/const.py | 3 ++ .../components/rhasspy/manifest.json | 9 ++++ homeassistant/components/rhasspy/strings.json | 12 +++++ .../components/rhasspy/translations/en.json | 12 +++++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 +++++ tests/components/rhasspy/__init__.py | 1 + tests/components/rhasspy/test_config_flow.py | 44 +++++++++++++++++++ tests/components/rhasspy/test_init.py | 26 +++++++++++ 13 files changed, 166 insertions(+) create mode 100644 homeassistant/components/rhasspy/__init__.py create mode 100644 homeassistant/components/rhasspy/config_flow.py create mode 100644 homeassistant/components/rhasspy/const.py create mode 100644 homeassistant/components/rhasspy/manifest.json create mode 100644 homeassistant/components/rhasspy/strings.json create mode 100644 homeassistant/components/rhasspy/translations/en.json create mode 100644 tests/components/rhasspy/__init__.py create mode 100644 tests/components/rhasspy/test_config_flow.py create mode 100644 tests/components/rhasspy/test_init.py diff --git a/.strict-typing b/.strict-typing index becd5cdda9a..9792f401ac4 100644 --- a/.strict-typing +++ b/.strict-typing @@ -193,6 +193,7 @@ homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.resolution_center.* +homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* diff --git a/CODEOWNERS b/CODEOWNERS index 4e5d07f54d8..a473d07af9d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -865,6 +865,8 @@ build.json @home-assistant/supervisor /tests/components/rflink/ @javicalle /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 /tests/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 +/homeassistant/components/rhasspy/ @balloob @synesthesiam +/tests/components/rhasspy/ @balloob @synesthesiam /homeassistant/components/ridwell/ @bachya /tests/components/ridwell/ @bachya /homeassistant/components/ring/ @balloob diff --git a/homeassistant/components/rhasspy/__init__.py b/homeassistant/components/rhasspy/__init__.py new file mode 100644 index 00000000000..669d81952d4 --- /dev/null +++ b/homeassistant/components/rhasspy/__init__.py @@ -0,0 +1,15 @@ +"""The Rhasspy integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Rhasspy from a config entry.""" + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return True diff --git a/homeassistant/components/rhasspy/config_flow.py b/homeassistant/components/rhasspy/config_flow.py new file mode 100644 index 00000000000..69ed802f817 --- /dev/null +++ b/homeassistant/components/rhasspy/config_flow.py @@ -0,0 +1,29 @@ +"""Config flow for Rhasspy integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Rhasspy.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is None: + return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + + return self.async_create_entry(title="Rhasspy", data={}) diff --git a/homeassistant/components/rhasspy/const.py b/homeassistant/components/rhasspy/const.py new file mode 100644 index 00000000000..127f20032ac --- /dev/null +++ b/homeassistant/components/rhasspy/const.py @@ -0,0 +1,3 @@ +"""Constants for the Rhasspy integration.""" + +DOMAIN = "rhasspy" diff --git a/homeassistant/components/rhasspy/manifest.json b/homeassistant/components/rhasspy/manifest.json new file mode 100644 index 00000000000..8b11e231b8c --- /dev/null +++ b/homeassistant/components/rhasspy/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "rhasspy", + "name": "Rhasspy", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/rhasspy", + "dependencies": ["intent"], + "codeowners": ["@balloob", "@synesthesiam"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/rhasspy/strings.json b/homeassistant/components/rhasspy/strings.json new file mode 100644 index 00000000000..4d2111ebd8a --- /dev/null +++ b/homeassistant/components/rhasspy/strings.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "description": "Do you want to enable Rhasspy support?" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/rhasspy/translations/en.json b/homeassistant/components/rhasspy/translations/en.json new file mode 100644 index 00000000000..af826fbf27d --- /dev/null +++ b/homeassistant/components/rhasspy/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to enable Rhasspy support?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d3be2c5e674..3c02842e856 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -290,6 +290,7 @@ FLOWS = { "recollect_waste", "renault", "rfxtrx", + "rhasspy", "ridwell", "ring", "risco", diff --git a/mypy.ini b/mypy.ini index 65b51e9d1c9..1fb12c3a47e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1886,6 +1886,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rhasspy.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ridwell.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/rhasspy/__init__.py b/tests/components/rhasspy/__init__.py new file mode 100644 index 00000000000..521c5166040 --- /dev/null +++ b/tests/components/rhasspy/__init__.py @@ -0,0 +1 @@ +"""Tests for the Rhasspy integration.""" diff --git a/tests/components/rhasspy/test_config_flow.py b/tests/components/rhasspy/test_config_flow.py new file mode 100644 index 00000000000..53c82c0cecd --- /dev/null +++ b/tests/components/rhasspy/test_config_flow.py @@ -0,0 +1,44 @@ +"""Test the Rhasspy config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.rhasspy.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.rhasspy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Rhasspy" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_single_entry(hass: HomeAssistant) -> None: + """Test we only allow single entry.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/rhasspy/test_init.py b/tests/components/rhasspy/test_init.py new file mode 100644 index 00000000000..e4f0b346347 --- /dev/null +++ b/tests/components/rhasspy/test_init.py @@ -0,0 +1,26 @@ +"""Tests for the Rhasspy integration.""" +from homeassistant.components.rhasspy.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry(hass: HomeAssistant) -> None: + """Test the Rhasspy configuration entry loading/unloading.""" + mock_config_entry = MockConfigEntry( + title="Rhasspy", + domain=DOMAIN, + data={}, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED From 01ca7f657ca16f2a6b5c8818f55fbc84a0f6ad3a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 12 Jul 2022 01:43:32 +1000 Subject: [PATCH 371/820] Shorten Entity Name in Aussie Broadband (#74946) --- homeassistant/components/aussie_broadband/sensor.py | 3 ++- tests/components/aussie_broadband/common.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py index ecc891deba9..648abaccb0c 100644 --- a/homeassistant/components/aussie_broadband/sensor.py +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +import re from typing import Any, cast from homeassistant.components.sensor import ( @@ -152,7 +153,7 @@ class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, service[SERVICE_ID])}, manufacturer="Aussie Broadband", configuration_url=f"https://my.aussiebroadband.com.au/#/{service['name'].lower()}/{service[SERVICE_ID]}/", - name=service["description"], + name=re.sub(r" - AVC\d+$", "", service["description"]), model=service["name"], ) diff --git a/tests/components/aussie_broadband/common.py b/tests/components/aussie_broadband/common.py index abb99355ef3..5d050388b38 100644 --- a/tests/components/aussie_broadband/common.py +++ b/tests/components/aussie_broadband/common.py @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry FAKE_SERVICES = [ { "service_id": "12345678", - "description": "Fake ABB NBN Service", + "description": "Fake ABB NBN Service - AVC123456789", "type": "NBN", "name": "NBN", }, From 924dce1b86c271513fc03985ddba7eab677a8a6b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 17:44:07 +0200 Subject: [PATCH 372/820] Log warning if number entities set _attr_unit_of_measurement (#74987) --- homeassistant/components/number/__init__.py | 4 +++- homeassistant/components/zha/number.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 1820e28bc4c..8c1800697a3 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -192,9 +192,10 @@ class NumberEntity(Entity): entity_description: NumberEntityDescription _attr_max_value: None _attr_min_value: None + _attr_mode: NumberMode = NumberMode.AUTO _attr_state: None = None _attr_step: None - _attr_mode: NumberMode = NumberMode.AUTO + _attr_unit_of_measurement: None # Subclasses of NumberEntity should not set this _attr_value: None _attr_native_max_value: float _attr_native_min_value: float @@ -369,6 +370,7 @@ class NumberEntity(Entity): return self._number_option_unit_of_measurement if hasattr(self, "_attr_unit_of_measurement"): + self._report_deprecated_number_entity() return self._attr_unit_of_measurement if ( hasattr(self, "entity_description") diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 36fc5267bd9..76b1121c2f0 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -521,7 +521,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0x257 - _attr_unit_of_measurement: str | None = UNITS[72] + _attr_native_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "timer_duration" @@ -533,5 +533,5 @@ class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time") _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0xFFFFFFFF - _attr_unit_of_measurement: str | None = UNITS[72] + _attr_native_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "filter_life_time" From 63706d2f67f47de2fa5fe62fc2be0555f2d4658b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:46:32 +0200 Subject: [PATCH 373/820] Remove blueprint from mypy ignore list (#74990) --- homeassistant/components/blueprint/errors.py | 10 ++++++---- homeassistant/components/blueprint/importer.py | 11 +++++++---- homeassistant/components/blueprint/models.py | 13 +++++++------ homeassistant/components/blueprint/websocket_api.py | 8 ++++---- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index 4b14201652f..aceca533d23 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError class BlueprintException(HomeAssistantError): """Base exception for blueprint errors.""" - def __init__(self, domain: str, msg: str) -> None: + def __init__(self, domain: str | None, msg: str) -> None: """Initialize a blueprint exception.""" super().__init__(msg) self.domain = domain @@ -22,7 +22,9 @@ class BlueprintException(HomeAssistantError): class BlueprintWithNameException(BlueprintException): """Base exception for blueprint errors.""" - def __init__(self, domain: str, blueprint_name: str, msg: str) -> None: + def __init__( + self, domain: str | None, blueprint_name: str | None, msg: str + ) -> None: """Initialize blueprint exception.""" super().__init__(domain, msg) self.blueprint_name = blueprint_name @@ -41,8 +43,8 @@ class InvalidBlueprint(BlueprintWithNameException): def __init__( self, - domain: str, - blueprint_name: str, + domain: str | None, + blueprint_name: str | None, blueprint_data: Any, msg_or_exc: vol.Invalid, ) -> None: diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index de39741d8ed..f8b37a97c31 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -91,12 +91,12 @@ def _get_community_post_import_url(url: str) -> str: def _extract_blueprint_from_community_topic( url: str, topic: dict, -) -> ImportedBlueprint | None: +) -> ImportedBlueprint: """Extract a blueprint from a community post JSON. Async friendly. """ - block_content = None + block_content: str blueprint = None post = topic["post_stream"]["posts"][0] @@ -118,6 +118,7 @@ def _extract_blueprint_from_community_topic( if not is_blueprint_config(data): continue + assert isinstance(data, dict) blueprint = Blueprint(data) break @@ -134,7 +135,7 @@ def _extract_blueprint_from_community_topic( async def fetch_blueprint_from_community_post( hass: HomeAssistant, url: str -) -> ImportedBlueprint | None: +) -> ImportedBlueprint: """Get blueprints from a community post url. Method can raise aiohttp client exceptions, vol.Invalid. @@ -160,6 +161,7 @@ async def fetch_blueprint_from_github_url( resp = await session.get(import_url, raise_for_status=True) raw_yaml = await resp.text() data = yaml.parse_yaml(raw_yaml) + assert isinstance(data, dict) blueprint = Blueprint(data) parsed_import_url = yarl.URL(import_url) @@ -189,7 +191,7 @@ async def fetch_blueprint_from_github_gist_url( blueprint = None filename = None - content = None + content: str for filename, info in gist["files"].items(): if not filename.endswith(".yaml"): @@ -200,6 +202,7 @@ async def fetch_blueprint_from_github_gist_url( if not is_blueprint_config(data): continue + assert isinstance(data, dict) blueprint = Blueprint(data) break diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index a8146764710..0d90c663b4f 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -188,7 +188,7 @@ class DomainBlueprints: self.hass = hass self.domain = domain self.logger = logger - self._blueprints = {} + self._blueprints: dict[str, Blueprint | None] = {} self._load_lock = asyncio.Lock() hass.data.setdefault(DOMAIN, {})[domain] = self @@ -216,19 +216,20 @@ class DomainBlueprints: except HomeAssistantError as err: raise FailedToLoad(self.domain, blueprint_path, err) from err + assert isinstance(blueprint_data, dict) return Blueprint( blueprint_data, expected_domain=self.domain, path=blueprint_path ) - def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException]: + def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException | None]: """Load all the blueprints.""" blueprint_folder = pathlib.Path( self.hass.config.path(BLUEPRINT_FOLDER, self.domain) ) - results = {} + results: dict[str, Blueprint | BlueprintException | None] = {} - for blueprint_path in blueprint_folder.glob("**/*.yaml"): - blueprint_path = str(blueprint_path.relative_to(blueprint_folder)) + for path in blueprint_folder.glob("**/*.yaml"): + blueprint_path = str(path.relative_to(blueprint_folder)) if self._blueprints.get(blueprint_path) is None: try: self._blueprints[blueprint_path] = self._load_blueprint( @@ -245,7 +246,7 @@ class DomainBlueprints: async def async_get_blueprints( self, - ) -> dict[str, Blueprint | BlueprintException]: + ) -> dict[str, Blueprint | BlueprintException | None]: """Get all the blueprints.""" async with self._load_lock: return await self.hass.async_add_executor_job(self._load_blueprints) diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index b8a4c214a2e..0b84d1d08c2 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -24,13 +24,13 @@ def async_setup(hass: HomeAssistant): websocket_api.async_register_command(hass, ws_delete_blueprint) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/list", vol.Required("domain"): cv.string, } ) +@websocket_api.async_response async def ws_list_blueprints(hass, connection, msg): """List available blueprints.""" domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get( @@ -55,13 +55,13 @@ async def ws_list_blueprints(hass, connection, msg): connection.send_result(msg["id"], results) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/import", vol.Required("url"): cv.url, } ) +@websocket_api.async_response async def ws_import_blueprint(hass, connection, msg): """Import a blueprint.""" async with async_timeout.timeout(10): @@ -86,7 +86,6 @@ async def ws_import_blueprint(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/save", @@ -96,6 +95,7 @@ async def ws_import_blueprint(hass, connection, msg): vol.Optional("source_url"): cv.url, } ) +@websocket_api.async_response async def ws_save_blueprint(hass, connection, msg): """Save a blueprint.""" @@ -135,7 +135,6 @@ async def ws_save_blueprint(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/delete", @@ -143,6 +142,7 @@ async def ws_save_blueprint(hass, connection, msg): vol.Required("path"): cv.path, } ) +@websocket_api.async_response async def ws_delete_blueprint(hass, connection, msg): """Delete a blueprint.""" diff --git a/mypy.ini b/mypy.ini index 1fb12c3a47e..4b74049f084 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2656,15 +2656,6 @@ no_implicit_optional = false warn_return_any = false warn_unreachable = false -[mypy-homeassistant.components.blueprint.importer] -ignore_errors = true - -[mypy-homeassistant.components.blueprint.models] -ignore_errors = true - -[mypy-homeassistant.components.blueprint.websocket_api] -ignore_errors = true - [mypy-homeassistant.components.cloud.client] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 17ef8edc529..a2c0fb37629 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,9 +16,6 @@ from .model import Config, Integration # remove your component from this list to enable type checks. # Do your best to not add anything new here. IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.blueprint.importer", - "homeassistant.components.blueprint.models", - "homeassistant.components.blueprint.websocket_api", "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", "homeassistant.components.conversation", From 7e0515b11931f5957c4fa2c03f3185710dfc96a8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:47:33 +0200 Subject: [PATCH 374/820] Remove conversation from mypy ignore list (#74991) --- homeassistant/components/conversation/__init__.py | 6 +++--- homeassistant/components/conversation/agent.py | 2 +- homeassistant/components/conversation/default_agent.py | 4 +++- mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index d04878cae4e..9fd6d1ad3e2 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -80,10 +80,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@websocket_api.async_response @websocket_api.websocket_command( {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str} ) +@websocket_api.async_response async def websocket_process(hass, connection, msg): """Process text.""" connection.send_result( @@ -94,8 +94,8 @@ async def websocket_process(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command({"type": "conversation/agent/info"}) +@websocket_api.async_response async def websocket_get_agent_info(hass, connection, msg): """Do we need onboarding.""" agent = await _get_agent(hass) @@ -109,8 +109,8 @@ async def websocket_get_agent_info(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) +@websocket_api.async_response async def websocket_set_onboarding(hass, connection, msg): """Set onboarding status.""" agent = await _get_agent(hass) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 56cf4aecdea..a19ae6d697b 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -26,5 +26,5 @@ class AbstractConversationAgent(ABC): @abstractmethod async def async_process( self, text: str, context: Context, conversation_id: str | None = None - ) -> intent.IntentResponse: + ) -> intent.IntentResponse | None: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 6d8d29ab086..9079f7893ec 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -112,7 +112,7 @@ class DefaultAgent(AbstractConversationAgent): async def async_process( self, text: str, context: core.Context, conversation_id: str | None = None - ) -> intent.IntentResponse: + ) -> intent.IntentResponse | None: """Process a sentence.""" intents = self.hass.data[DOMAIN] @@ -129,3 +129,5 @@ class DefaultAgent(AbstractConversationAgent): text, context, ) + + return None diff --git a/mypy.ini b/mypy.ini index 4b74049f084..cccfdc55091 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2662,12 +2662,6 @@ ignore_errors = true [mypy-homeassistant.components.cloud.http_api] ignore_errors = true -[mypy-homeassistant.components.conversation] -ignore_errors = true - -[mypy-homeassistant.components.conversation.default_agent] -ignore_errors = true - [mypy-homeassistant.components.evohome] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a2c0fb37629..8ddfa2b53a4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -18,8 +18,6 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", - "homeassistant.components.conversation", - "homeassistant.components.conversation.default_agent", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", "homeassistant.components.lovelace", From 964bb63da66c4e59eb2e88bb34070643a1935881 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:12:56 +0200 Subject: [PATCH 375/820] Migrate Spotify to new entity naming style (#74992) --- homeassistant/components/spotify/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 7d24f1deee8..04f523c2d4b 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -107,10 +107,11 @@ def spotify_exception_handler(func): class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" + _attr_entity_registry_enabled_default = False + _attr_has_entity_name = True _attr_icon = "mdi:spotify" _attr_media_content_type = MEDIA_TYPE_MUSIC _attr_media_image_remotely_accessible = False - _attr_entity_registry_enabled_default = False def __init__( self, @@ -122,7 +123,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self._id = user_id self.data = data - self._attr_name = f"Spotify {name}" self._attr_unique_id = user_id if self.data.current_user["product"] == "premium": From a30d7e51044eeef99a3e4cb368a626fee098116c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:13:09 +0200 Subject: [PATCH 376/820] Migrate Speedtest.net to new entity naming style (#75004) --- homeassistant/components/speedtestdotnet/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 4f16c012fa6..44b018e1e19 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -49,6 +49,7 @@ class SpeedtestSensor( """Implementation of a speedtest.net sensor.""" entity_description: SpeedtestSensorEntityDescription + _attr_has_entity_name = True _attr_icon = ICON def __init__( @@ -59,7 +60,6 @@ class SpeedtestSensor( """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" self._attr_unique_id = description.key self._state: StateType = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} From 4a39087fe7be56b463ff9d67360531554bc2a1c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:14:17 +0200 Subject: [PATCH 377/820] Migrate Verisure to new entity naming style (#74997) --- .../verisure/alarm_control_panel.py | 2 +- .../components/verisure/binary_sensor.py | 5 ++-- homeassistant/components/verisure/camera.py | 3 ++- homeassistant/components/verisure/lock.py | 4 ++-- homeassistant/components/verisure/sensor.py | 24 +++++-------------- homeassistant/components/verisure/switch.py | 4 ++-- 6 files changed, 16 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 8f92ee0a5dd..5030e01c8b1 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -33,7 +33,7 @@ class VerisureAlarm( """Representation of a Verisure alarm status.""" _attr_code_format = CodeFormat.NUMBER - _attr_name = "Verisure Alarm" + _attr_has_entity_name = True _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 217890b8a01..45e29cfa5f1 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -39,13 +39,13 @@ class VerisureDoorWindowSensor( """Representation of a Verisure door window sensor.""" _attr_device_class = BinarySensorDeviceClass.OPENING + _attr_has_entity_name = True def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure door window sensor.""" super().__init__(coordinator) - self._attr_name = coordinator.data["door_window"][serial_number]["area"] self._attr_unique_id = f"{serial_number}_door_window" self.serial_number = serial_number @@ -84,9 +84,10 @@ class VerisureEthernetStatus( ): """Representation of a Verisure VBOX internet status.""" - _attr_name = "Verisure Ethernet status" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + _attr_name = "Ethernet status" @property def unique_id(self) -> str: diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index c753bf2c5dc..98ed41c5b9f 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -46,6 +46,8 @@ async def async_setup_entry( class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera): """Representation of a Verisure camera.""" + _attr_has_entity_name = True + def __init__( self, coordinator: VerisureDataUpdateCoordinator, @@ -56,7 +58,6 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera) super().__init__(coordinator) Camera.__init__(self) - self._attr_name = coordinator.data["cameras"][serial_number]["area"] self._attr_unique_id = serial_number self.serial_number = serial_number diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 8074cf28f32..02cdad158ca 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -59,13 +59,13 @@ async def async_setup_entry( class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEntity): """Representation of a Verisure doorlock.""" + _attr_has_entity_name = True + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure lock.""" super().__init__(coordinator) - - self._attr_name = coordinator.data["locks"][serial_number]["area"] self._attr_unique_id = serial_number self.serial_number = serial_number diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 3b8f722c6f7..676082e4cda 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -51,6 +51,8 @@ class VerisureThermometer( """Representation of a Verisure thermometer.""" _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_has_entity_name = True + _attr_name = "Temperature" _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_state_class = SensorStateClass.MEASUREMENT @@ -62,12 +64,6 @@ class VerisureThermometer( self._attr_unique_id = f"{serial_number}_temperature" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of the entity.""" - name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] - return f"{name} Temperature" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -106,6 +102,8 @@ class VerisureHygrometer( """Representation of a Verisure hygrometer.""" _attr_device_class = SensorDeviceClass.HUMIDITY + _attr_has_entity_name = True + _attr_name = "Humidity" _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = SensorStateClass.MEASUREMENT @@ -117,12 +115,6 @@ class VerisureHygrometer( self._attr_unique_id = f"{serial_number}_humidity" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of the entity.""" - name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] - return f"{name} Humidity" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -160,6 +152,8 @@ class VerisureMouseDetection( ): """Representation of a Verisure mouse detector.""" + _attr_name = "Mouse" + _attr_has_entity_name = True _attr_native_unit_of_measurement = "Mice" def __init__( @@ -170,12 +164,6 @@ class VerisureMouseDetection( self._attr_unique_id = f"{serial_number}_mice" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of the entity.""" - name = self.coordinator.data["mice"][self.serial_number]["area"] - return f"{name} Mouse" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 5d1fd728f4a..177beb4272b 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -30,13 +30,13 @@ async def async_setup_entry( class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], SwitchEntity): """Representation of a Verisure smartplug.""" + _attr_has_entity_name = True + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure device.""" super().__init__(coordinator) - - self._attr_name = coordinator.data["smart_plugs"][serial_number]["area"] self._attr_unique_id = serial_number self.serial_number = serial_number From 75abf87611d8ad5627126a3fe09fdddc8402237c Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 11 Jul 2022 18:16:29 +0200 Subject: [PATCH 378/820] Migrate Fronius to new entity naming style (#74974) --- homeassistant/components/fronius/sensor.py | 32 +- tests/components/fronius/test_sensor.py | 541 +++++++++------------ 2 files changed, 234 insertions(+), 339 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index c3b219c4b22..35881225b68 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, Final from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -109,14 +108,14 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="current_ac", - name="AC current", + name="Current AC", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_dc", - name="DC current", + name="Current DC", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -124,7 +123,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="current_dc_2", - name="DC current 2", + name="Current DC 2", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -132,14 +131,14 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="power_ac", - name="AC power", + name="Power AC", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_ac", - name="AC voltage", + name="Voltage AC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -147,7 +146,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="voltage_dc", - name="DC voltage", + name="Voltage DC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -155,7 +154,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="voltage_dc_2", - name="DC voltage 2", + name="Voltage DC 2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -492,7 +491,7 @@ OHMPILOT_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="temperature_channel_1", - name="Temperature Channel 1", + name="Temperature channel 1", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -541,7 +540,7 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="meter_mode", - name="Mode", + name="Meter mode", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( @@ -656,7 +655,8 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn """Defines a Fronius coordinator entity.""" entity_descriptions: list[SensorEntityDescription] - _entity_id_prefix: str + + _attr_has_entity_name = True def __init__( self, @@ -669,10 +669,6 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn self.entity_description = next( desc for desc in self.entity_descriptions if desc.key == key ) - # default entity_id added 2021.12 - # used for migration from non-unique_id entities of previous integration implementation - # when removed after migration period `_entity_id_prefix` will also no longer be needed - self.entity_id = f"{SENSOR_DOMAIN}.{key}_{DOMAIN}_{self._entity_id_prefix}_{coordinator.solar_net.host}" self.solar_net_id = solar_net_id self._attr_native_value = self._get_entity_value() @@ -709,7 +705,6 @@ class InverterSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius inverter sensor.""" - self._entity_id_prefix = f"inverter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) # device_info created in __init__ from a `GetInverterInfo` request self._attr_device_info = coordinator.inverter_info.device_info @@ -720,7 +715,6 @@ class LoggerSensor(_FroniusSensorEntity): """Defines a Fronius logger device sensor entity.""" entity_descriptions = LOGGER_ENTITY_DESCRIPTIONS - _entity_id_prefix = "logger_info_0" def __init__( self, @@ -749,7 +743,6 @@ class MeterSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" - self._entity_id_prefix = f"meter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) meter_data = self._device_data() # S0 meters connected directly to inverters respond "n.a." as serial number @@ -782,7 +775,6 @@ class OhmpilotSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" - self._entity_id_prefix = f"ohmpilot_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) device_data = self._device_data() @@ -801,7 +793,6 @@ class PowerFlowSensor(_FroniusSensorEntity): """Defines a Fronius power flow sensor entity.""" entity_descriptions = POWER_FLOW_ENTITY_DESCRIPTIONS - _entity_id_prefix = "power_flow_0" def __init__( self, @@ -830,7 +821,6 @@ class StorageSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius storage sensor.""" - self._entity_id_prefix = f"storage_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) storage_data = self._device_data() diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index 0f3e8f28a56..3ed1e37505c 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -30,11 +30,11 @@ async def test_symo_inverter(hass, aioclient_mock): hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 10828) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44186900) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 25507686) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 16) + assert_state("sensor.symo_20_current_dc", 0) + assert_state("sensor.symo_20_energy_day", 10828) + assert_state("sensor.symo_20_energy_total", 44186900) + assert_state("sensor.symo_20_energy_year", 25507686) + assert_state("sensor.symo_20_voltage_dc", 16) # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) @@ -48,15 +48,15 @@ async def test_symo_inverter(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # 4 additional AC entities - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 2.19) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 1113) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44188000) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 25508798) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 518) - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 5.19) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.94) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 1190) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.90) + assert_state("sensor.symo_20_current_dc", 2.19) + assert_state("sensor.symo_20_energy_day", 1113) + assert_state("sensor.symo_20_energy_total", 44188000) + assert_state("sensor.symo_20_energy_year", 25508798) + assert_state("sensor.symo_20_voltage_dc", 518) + assert_state("sensor.symo_20_current_ac", 5.19) + assert_state("sensor.symo_20_frequency_ac", 49.94) + assert_state("sensor.symo_20_power_ac", 1190) + assert_state("sensor.symo_20_voltage_ac", 227.90) # Third test at nighttime - additional AC entities aren't changed mock_responses(aioclient_mock, night=True) @@ -64,10 +64,10 @@ async def test_symo_inverter(hass, aioclient_mock): hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 5.19) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.94) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 1190) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.90) + assert_state("sensor.symo_20_current_ac", 5.19) + assert_state("sensor.symo_20_frequency_ac", 49.94) + assert_state("sensor.symo_20_power_ac", 1190) + assert_state("sensor.symo_20_voltage_ac", 227.90) async def test_symo_logger(hass, aioclient_mock): @@ -82,18 +82,9 @@ async def test_symo_logger(hass, aioclient_mock): await setup_fronius_integration(hass) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24 # states are rounded to 4 decimals - assert_state( - "sensor.cash_factor_fronius_logger_info_0_http_fronius", - 0.078, - ) - assert_state( - "sensor.co2_factor_fronius_logger_info_0_http_fronius", - 0.53, - ) - assert_state( - "sensor.delivery_factor_fronius_logger_info_0_http_fronius", - 0.15, - ) + assert_state("sensor.solarnet_grid_export_tariff", 0.078) + assert_state("sensor.solarnet_co2_factor", 0.53) + assert_state("sensor.solarnet_grid_import_tariff", 0.15) async def test_symo_meter(hass, aioclient_mock): @@ -113,48 +104,38 @@ async def test_symo_meter(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # states are rounded to 4 decimals - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 7.755) - assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 6.68) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 10.102) - assert_state( - "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 59960790 - ) - assert_state( - "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 723160 - ) - assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 35623065) - assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 15303334) - assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 15303334) - assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 35623065) - assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 50) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 1772.793) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 1527.048) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 2333.562) - assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 5592.57) - assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", -0.99) - assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", -0.99) - assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.99) - assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 1) - assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", 51.48) - assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", 115.63) - assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -164.24) - assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", 2.87) - assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 1765.55) - assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 1515.8) - assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 2311.22) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", 5592.57) - assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 228.6) - assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 228.6) - assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 231) - assert_state( - "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 395.9 - ) - assert_state( - "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 398 - ) - assert_state( - "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 398 - ) + assert_state("sensor.smart_meter_63a_current_ac_phase_1", 7.755) + assert_state("sensor.smart_meter_63a_current_ac_phase_2", 6.68) + assert_state("sensor.smart_meter_63a_current_ac_phase_3", 10.102) + assert_state("sensor.smart_meter_63a_energy_reactive_ac_consumed", 59960790) + assert_state("sensor.smart_meter_63a_energy_reactive_ac_produced", 723160) + assert_state("sensor.smart_meter_63a_energy_real_ac_minus", 35623065) + assert_state("sensor.smart_meter_63a_energy_real_ac_plus", 15303334) + assert_state("sensor.smart_meter_63a_energy_real_consumed", 15303334) + assert_state("sensor.smart_meter_63a_energy_real_produced", 35623065) + assert_state("sensor.smart_meter_63a_frequency_phase_average", 50) + assert_state("sensor.smart_meter_63a_power_apparent_phase_1", 1772.793) + assert_state("sensor.smart_meter_63a_power_apparent_phase_2", 1527.048) + assert_state("sensor.smart_meter_63a_power_apparent_phase_3", 2333.562) + assert_state("sensor.smart_meter_63a_power_apparent", 5592.57) + assert_state("sensor.smart_meter_63a_power_factor_phase_1", -0.99) + assert_state("sensor.smart_meter_63a_power_factor_phase_2", -0.99) + assert_state("sensor.smart_meter_63a_power_factor_phase_3", 0.99) + assert_state("sensor.smart_meter_63a_power_factor", 1) + assert_state("sensor.smart_meter_63a_power_reactive_phase_1", 51.48) + assert_state("sensor.smart_meter_63a_power_reactive_phase_2", 115.63) + assert_state("sensor.smart_meter_63a_power_reactive_phase_3", -164.24) + assert_state("sensor.smart_meter_63a_power_reactive", 2.87) + assert_state("sensor.smart_meter_63a_power_real_phase_1", 1765.55) + assert_state("sensor.smart_meter_63a_power_real_phase_2", 1515.8) + assert_state("sensor.smart_meter_63a_power_real_phase_3", 2311.22) + assert_state("sensor.smart_meter_63a_power_real", 5592.57) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_1", 228.6) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_2", 228.6) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_3", 231) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_1_2", 395.9) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_2_3", 398) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_3_1", 398) async def test_symo_power_flow(hass, aioclient_mock): @@ -175,30 +156,12 @@ async def test_symo_power_flow(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # states are rounded to 4 decimals - assert_state( - "sensor.energy_day_fronius_power_flow_0_http_fronius", - 10828, - ) - assert_state( - "sensor.energy_total_fronius_power_flow_0_http_fronius", - 44186900, - ) - assert_state( - "sensor.energy_year_fronius_power_flow_0_http_fronius", - 25507686, - ) - assert_state( - "sensor.power_grid_fronius_power_flow_0_http_fronius", - 975.31, - ) - assert_state( - "sensor.power_load_fronius_power_flow_0_http_fronius", - -975.31, - ) - assert_state( - "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", - 0, - ) + assert_state("sensor.solarnet_energy_day", 10828) + assert_state("sensor.solarnet_energy_total", 44186900) + assert_state("sensor.solarnet_energy_year", 25507686) + assert_state("sensor.solarnet_power_grid", 975.31) + assert_state("sensor.solarnet_power_load", -975.31) + assert_state("sensor.solarnet_relative_autonomy", 0) # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) @@ -208,38 +171,14 @@ async def test_symo_power_flow(hass, aioclient_mock): await hass.async_block_till_done() # 54 because power_flow `rel_SelfConsumption` and `P_PV` is not `null` anymore assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 - assert_state( - "sensor.energy_day_fronius_power_flow_0_http_fronius", - 1101.7001, - ) - assert_state( - "sensor.energy_total_fronius_power_flow_0_http_fronius", - 44188000, - ) - assert_state( - "sensor.energy_year_fronius_power_flow_0_http_fronius", - 25508788, - ) - assert_state( - "sensor.power_grid_fronius_power_flow_0_http_fronius", - 1703.74, - ) - assert_state( - "sensor.power_load_fronius_power_flow_0_http_fronius", - -2814.74, - ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", - 1111, - ) - assert_state( - "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", - 39.4708, - ) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", - 100, - ) + assert_state("sensor.solarnet_energy_day", 1101.7001) + assert_state("sensor.solarnet_energy_total", 44188000) + assert_state("sensor.solarnet_energy_year", 25508788) + assert_state("sensor.solarnet_power_grid", 1703.74) + assert_state("sensor.solarnet_power_load", -2814.74) + assert_state("sensor.solarnet_power_photovoltaics", 1111) + assert_state("sensor.solarnet_relative_autonomy", 39.4708) + assert_state("sensor.solarnet_relative_self_consumption", 100) async def test_gen24(hass, aioclient_mock): @@ -259,74 +198,60 @@ async def test_gen24(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # inverter 1 - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 0.1589) - assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.0754) - assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.0783) - assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 403.4312) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 37.3204) - assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 411.3811) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 1530193.42) - assert_state("sensor.inverter_state_fronius_inverter_1_http_fronius", "Running") - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 234.9168) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.9917) + assert_state("sensor.inverter_name_current_ac", 0.1589) + assert_state("sensor.inverter_name_current_dc_2", 0.0754) + assert_state("sensor.inverter_name_status_code", 7) + assert_state("sensor.inverter_name_current_dc", 0.0783) + assert_state("sensor.inverter_name_voltage_dc_2", 403.4312) + assert_state("sensor.inverter_name_power_ac", 37.3204) + assert_state("sensor.inverter_name_error_code", 0) + assert_state("sensor.inverter_name_voltage_dc", 411.3811) + assert_state("sensor.inverter_name_energy_total", 1530193.42) + assert_state("sensor.inverter_name_inverter_state", "Running") + assert_state("sensor.inverter_name_voltage_ac", 234.9168) + assert_state("sensor.inverter_name_frequency_ac", 49.9917) # meter - assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 3863340.0) - assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 2013105.0) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", 653.1) - assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 49.9) - assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 0.0) - assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 0.828) - assert_state( - "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 88221.0 - ) - assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 3863340.0) - assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 2.33) - assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 235.9) - assert_state( - "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 408.7 - ) - assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 294.9) - assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 2013105.0) - assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 236.1) - assert_state( - "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 1989125.0 - ) - assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 236.9) - assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", 0.441) - assert_state( - "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 409.6 - ) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 1.825) - assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.832) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 243.3) - assert_state( - "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 409.4 - ) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 323.4) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 301.2) - assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 106.8) - assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", 0.934) - assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 251.3) - assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", -218.6) - assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", -132.8) - assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -166.0) - assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 868.0) - assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", -517.4) - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 1.145) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_produced", 3863340.0) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_consumed", 2013105.0) + assert_state("sensor.smart_meter_ts_65a_3_power_real", 653.1) + assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9) + assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0) + assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.828) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_consumed", 88221.0) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_minus", 3863340.0) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_2", 2.33) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1", 235.9) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1_2", 408.7) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_2", 294.9) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_plus", 2013105.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2", 236.1) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_produced", 1989125.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3", 236.9) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_1", 0.441) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2_3", 409.6) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_3", 1.825) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_3", 0.832) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_1", 243.3) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3_1", 409.4) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_2", 323.4) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_3", 301.2) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_1", 106.8) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_2", 0.934) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_3", 251.3) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_1", -218.6) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_2", -132.8) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_3", -166.0) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent", 868.0) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive", -517.4) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_1", 1.145) # power_flow - assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 658.4) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100.0 - ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 62.9481 - ) - assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -695.6827) - assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "meter") - assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 5.3592) - assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 1530193.42) + assert_state("sensor.solarnet_power_grid", 658.4) + assert_state("sensor.solarnet_relative_self_consumption", 100.0) + assert_state("sensor.solarnet_power_photovoltaics", 62.9481) + assert_state("sensor.solarnet_power_load", -695.6827) + assert_state("sensor.solarnet_meter_mode", "meter") + assert_state("sensor.solarnet_relative_autonomy", 5.3592) + assert_state("sensor.solarnet_energy_total", 1530193.42) async def test_gen24_storage(hass, aioclient_mock): @@ -348,92 +273,74 @@ async def test_gen24_storage(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 64 # inverter 1 - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.3952) - assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 318.8103) - assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.3564) - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 1.1087) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 250.9093) - assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 7512794.0117) - assert_state("sensor.inverter_state_fronius_inverter_1_http_fronius", "Running") - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 419.1009) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.354) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.9816) + assert_state("sensor.gen24_storage_current_dc", 0.3952) + assert_state("sensor.gen24_storage_voltage_dc_2", 318.8103) + assert_state("sensor.gen24_storage_current_dc_2", 0.3564) + assert_state("sensor.gen24_storage_current_ac", 1.1087) + assert_state("sensor.gen24_storage_power_ac", 250.9093) + assert_state("sensor.gen24_storage_error_code", 0) + assert_state("sensor.gen24_storage_status_code", 7) + assert_state("sensor.gen24_storage_energy_total", 7512794.0117) + assert_state("sensor.gen24_storage_inverter_state", "Running") + assert_state("sensor.gen24_storage_voltage_dc", 419.1009) + assert_state("sensor.gen24_storage_voltage_ac", 227.354) + assert_state("sensor.gen24_storage_frequency_ac", 49.9816) # meter - assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 1705128.0) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", 487.7) - assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 0.698) - assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 1247204.0) - assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 49.9) - assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 0.0) - assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", -501.5) - assert_state( - "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 3266105.0 - ) - assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 19.6) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 0.645) - assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 1705128.0) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 383.9) - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 1.701) - assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 1.832) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 319.5) - assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 229.4) - assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 150.0) - assert_state( - "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 394.3 - ) - assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 225.6) - assert_state( - "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 5482.0 - ) - assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 1247204.0) - assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", 0.995) - assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.163) - assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", 0.389) - assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", -31.3) - assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -116.7) - assert_state( - "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 396.0 - ) - assert_state( - "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 393.0 - ) - assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", -353.4) - assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 317.9) - assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 228.3) - assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 821.9) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 118.4) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_produced", 1705128.0) + assert_state("sensor.smart_meter_ts_65a_3_power_real", 487.7) + assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.698) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_consumed", 1247204.0) + assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9) + assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive", -501.5) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_produced", 3266105.0) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_3", 19.6) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_3", 0.645) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_minus", 1705128.0) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_2", 383.9) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_1", 1.701) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_2", 1.832) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_1", 319.5) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1", 229.4) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_2", 150.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3_1", 394.3) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2", 225.6) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_consumed", 5482.0) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_plus", 1247204.0) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_1", 0.995) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_3", 0.163) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_2", 0.389) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_1", -31.3) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_3", -116.7) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1_2", 396.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2_3", 393.0) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_2", -353.4) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_1", 317.9) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3", 228.3) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent", 821.9) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_3", 118.4) # ohmpilot - assert_state( - "sensor.energy_real_ac_consumed_fronius_ohmpilot_0_http_fronius", 1233295.0 - ) - assert_state("sensor.power_real_ac_fronius_ohmpilot_0_http_fronius", 0.0) - assert_state("sensor.temperature_channel_1_fronius_ohmpilot_0_http_fronius", 38.9) - assert_state("sensor.state_code_fronius_ohmpilot_0_http_fronius", 0.0) - assert_state( - "sensor.state_message_fronius_ohmpilot_0_http_fronius", "Up and running" - ) + assert_state("sensor.ohmpilot_energy_consumed", 1233295.0) + assert_state("sensor.ohmpilot_power", 0.0) + assert_state("sensor.ohmpilot_temperature_channel_1", 38.9) + assert_state("sensor.ohmpilot_state_code", 0.0) + assert_state("sensor.ohmpilot_state_message", "Up and running") # power_flow - assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 2274.9) - assert_state("sensor.power_battery_fronius_power_flow_0_http_fronius", 0.1591) - assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2459.3092) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100.0 - ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 216.4328 - ) - assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 7.4984) - assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "bidirectional") - assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 7512664.4042) + assert_state("sensor.solarnet_power_grid", 2274.9) + assert_state("sensor.solarnet_power_battery", 0.1591) + assert_state("sensor.solarnet_power_load", -2459.3092) + assert_state("sensor.solarnet_relative_self_consumption", 100.0) + assert_state("sensor.solarnet_power_photovoltaics", 216.4328) + assert_state("sensor.solarnet_relative_autonomy", 7.4984) + assert_state("sensor.solarnet_meter_mode", "bidirectional") + assert_state("sensor.solarnet_energy_total", 7512664.4042) # storage - assert_state("sensor.current_dc_fronius_storage_0_http_fronius", 0.0) - assert_state("sensor.state_of_charge_fronius_storage_0_http_fronius", 4.6) - assert_state("sensor.capacity_maximum_fronius_storage_0_http_fronius", 16588) - assert_state("sensor.temperature_cell_fronius_storage_0_http_fronius", 21.5) - assert_state("sensor.capacity_designed_fronius_storage_0_http_fronius", 16588) - assert_state("sensor.voltage_dc_fronius_storage_0_http_fronius", 0.0) + assert_state("sensor.byd_battery_box_premium_hv_current_dc", 0.0) + assert_state("sensor.byd_battery_box_premium_hv_state_of_charge", 4.6) + assert_state("sensor.byd_battery_box_premium_hv_capacity_maximum", 16588) + assert_state("sensor.byd_battery_box_premium_hv_temperature_cell", 21.5) + assert_state("sensor.byd_battery_box_premium_hv_capacity_designed", 16588) + assert_state("sensor.byd_battery_box_premium_hv_voltage_dc", 0.0) # Devices device_registry = dr.async_get(hass) @@ -486,52 +393,50 @@ async def test_primo_s0(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 40 # logger - assert_state("sensor.cash_factor_fronius_logger_info_0_http_fronius", 1) - assert_state("sensor.co2_factor_fronius_logger_info_0_http_fronius", 0.53) - assert_state("sensor.delivery_factor_fronius_logger_info_0_http_fronius", 1) + assert_state("sensor.solarnet_grid_export_tariff", 1) + assert_state("sensor.solarnet_co2_factor", 0.53) + assert_state("sensor.solarnet_grid_import_tariff", 1) # inverter 1 - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 17114940) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 22504) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 452.3) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 862) - assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 4.23) - assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 7532755.5) - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 3.85) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 223.9) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 60) - assert_state("sensor.led_color_fronius_inverter_1_http_fronius", 2) - assert_state("sensor.led_state_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.primo_5_0_1_energy_total", 17114940) + assert_state("sensor.primo_5_0_1_energy_day", 22504) + assert_state("sensor.primo_5_0_1_voltage_dc", 452.3) + assert_state("sensor.primo_5_0_1_power_ac", 862) + assert_state("sensor.primo_5_0_1_error_code", 0) + assert_state("sensor.primo_5_0_1_current_dc", 4.23) + assert_state("sensor.primo_5_0_1_status_code", 7) + assert_state("sensor.primo_5_0_1_energy_year", 7532755.5) + assert_state("sensor.primo_5_0_1_current_ac", 3.85) + assert_state("sensor.primo_5_0_1_voltage_ac", 223.9) + assert_state("sensor.primo_5_0_1_frequency_ac", 60) + assert_state("sensor.primo_5_0_1_led_color", 2) + assert_state("sensor.primo_5_0_1_led_state", 0) # inverter 2 - assert_state("sensor.energy_total_fronius_inverter_2_http_fronius", 5796010) - assert_state("sensor.energy_day_fronius_inverter_2_http_fronius", 14237) - assert_state("sensor.voltage_dc_fronius_inverter_2_http_fronius", 329.5) - assert_state("sensor.power_ac_fronius_inverter_2_http_fronius", 296) - assert_state("sensor.error_code_fronius_inverter_2_http_fronius", 0) - assert_state("sensor.current_dc_fronius_inverter_2_http_fronius", 0.97) - assert_state("sensor.status_code_fronius_inverter_2_http_fronius", 7) - assert_state("sensor.energy_year_fronius_inverter_2_http_fronius", 3596193.25) - assert_state("sensor.current_ac_fronius_inverter_2_http_fronius", 1.32) - assert_state("sensor.voltage_ac_fronius_inverter_2_http_fronius", 223.6) - assert_state("sensor.frequency_ac_fronius_inverter_2_http_fronius", 60.01) - assert_state("sensor.led_color_fronius_inverter_2_http_fronius", 2) - assert_state("sensor.led_state_fronius_inverter_2_http_fronius", 0) + assert_state("sensor.primo_3_0_1_energy_total", 5796010) + assert_state("sensor.primo_3_0_1_energy_day", 14237) + assert_state("sensor.primo_3_0_1_voltage_dc", 329.5) + assert_state("sensor.primo_3_0_1_power_ac", 296) + assert_state("sensor.primo_3_0_1_error_code", 0) + assert_state("sensor.primo_3_0_1_current_dc", 0.97) + assert_state("sensor.primo_3_0_1_status_code", 7) + assert_state("sensor.primo_3_0_1_energy_year", 3596193.25) + assert_state("sensor.primo_3_0_1_current_ac", 1.32) + assert_state("sensor.primo_3_0_1_voltage_ac", 223.6) + assert_state("sensor.primo_3_0_1_frequency_ac", 60.01) + assert_state("sensor.primo_3_0_1_led_color", 2) + assert_state("sensor.primo_3_0_1_led_state", 0) # meter - assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 1) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", -2216.7487) + assert_state("sensor.s0_meter_at_inverter_1_meter_location", 1) + assert_state("sensor.s0_meter_at_inverter_1_power_real", -2216.7487) # power_flow - assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2218.9349) - assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "vague-meter") - assert_state("sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 1834) - assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 384.9349) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100 - ) - assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 82.6523) - assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 22910919.5) - assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", 36724) - assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", 11128933.25) + assert_state("sensor.solarnet_power_load", -2218.9349) + assert_state("sensor.solarnet_meter_mode", "vague-meter") + assert_state("sensor.solarnet_power_photovoltaics", 1834) + assert_state("sensor.solarnet_power_grid", 384.9349) + assert_state("sensor.solarnet_relative_self_consumption", 100) + assert_state("sensor.solarnet_relative_autonomy", 82.6523) + assert_state("sensor.solarnet_energy_total", 22910919.5) + assert_state("sensor.solarnet_energy_day", 36724) + assert_state("sensor.solarnet_energy_year", 11128933.25) # Devices device_registry = dr.async_get(hass) From 8d6925b3ab535c252ebce33f948d46d0c0927572 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:17:54 +0200 Subject: [PATCH 379/820] Migrate Tuya to new entity naming style (#74975) --- homeassistant/components/tuya/base.py | 11 +- .../components/tuya/binary_sensor.py | 10 +- homeassistant/components/tuya/button.py | 10 +- homeassistant/components/tuya/light.py | 2 +- homeassistant/components/tuya/number.py | 48 +++---- homeassistant/components/tuya/select.py | 48 +++---- homeassistant/components/tuya/sensor.py | 130 +++++++++--------- 7 files changed, 125 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 33b571ea848..624bfd80cd4 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -131,6 +131,7 @@ class ElectricityTypeData: class TuyaEntity(Entity): """Tuya base device.""" + _attr_has_entity_name = True _attr_should_poll = False def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: @@ -139,16 +140,6 @@ class TuyaEntity(Entity): self.device = device self.device_manager = device_manager - @property - def name(self) -> str | None: - """Return Tuya device name.""" - if ( - hasattr(self, "entity_description") - and self.entity_description.name is not None - ): - return f"{self.device.name} {self.entity_description.name}" - return self.device.name - @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index d5e4a9b22b0..5641a18022e 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -64,19 +64,19 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TuyaBinarySensorEntityDescription( key=DPCode.VOC_STATE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.PM25_STATE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.CO_STATE, - name="Carbon Monoxide", + name="Carbon monoxide", icon="mdi:molecule-co", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", @@ -84,7 +84,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.CO2_STATE, icon="mdi:molecule-co2", - name="Carbon Dioxide", + name="Carbon dioxide", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), @@ -101,7 +101,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TuyaBinarySensorEntityDescription( key=DPCode.WATERSENSOR_STATE, - name="Water Leak", + name="Water leak", device_class=BinarySensorDeviceClass.MOISTURE, on_value="alarm", ), diff --git a/homeassistant/components/tuya/button.py b/homeassistant/components/tuya/button.py index 26014e53b74..9eef7b03452 100644 --- a/homeassistant/components/tuya/button.py +++ b/homeassistant/components/tuya/button.py @@ -22,31 +22,31 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { "sd": ( ButtonEntityDescription( key=DPCode.RESET_DUSTER_CLOTH, - name="Reset Duster Cloth", + name="Reset duster cloth", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_EDGE_BRUSH, - name="Reset Edge Brush", + name="Reset edge brush", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_FILTER, - name="Reset Filter", + name="Reset filter", icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_MAP, - name="Reset Map", + name="Reset map", icon="mdi:map-marker-remove", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_ROLL_BRUSH, - name="Reset Roll Brush", + name="Reset roll brush", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 7ba8acdc0fe..9b78008af55 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -299,7 +299,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { ), TuyaLightEntityDescription( key=DPCode.SWITCH_NIGHT_LIGHT, - name="Night Light", + name="Night light", ), ), # Remote Control diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index e7712dcf630..39874d0ae8d 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -51,21 +51,21 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.TEMP_BOILING_C, - name="Temperature After Boiling", + name="Temperature after boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_F, - name="Temperature After Boiling", + name="Temperature after boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, - name="Heat Preservation Time", + name="Heat preservation time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), @@ -80,7 +80,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.VOICE_TIMES, - name="Voice Times", + name="Voice times", icon="mdi:microphone", ), ), @@ -94,13 +94,13 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.NEAR_DETECTION, - name="Near Detection", + name="Near detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.FAR_DETECTION, - name="Far Detection", + name="Far detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), @@ -110,7 +110,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "kfj": ( NumberEntityDescription( key=DPCode.WATER_SET, - name="Water Level", + name="Water level", icon="mdi:cup-water", entity_category=EntityCategory.CONFIG, ), @@ -123,7 +123,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.WARM_TIME, - name="Heat Preservation Time", + name="Heat preservation time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), @@ -138,20 +138,20 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "mzj": ( NumberEntityDescription( key=DPCode.COOK_TEMPERATURE, - name="Cook Temperature", + name="Cook temperature", icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.COOK_TIME, - name="Cook Time", + name="Cook time", icon="mdi:timer", native_unit_of_measurement=TIME_MINUTES, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLOUD_RECIPE_NUMBER, - name="Cloud Recipe", + name="Cloud recipe", entity_category=EntityCategory.CONFIG, ), ), @@ -189,37 +189,37 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "tgkg": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, - name="Minimum Brightness", + name="Minimum brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, - name="Maximum Brightness", + name="Maximum brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, - name="Minimum Brightness 2", + name="Minimum brightness 2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, - name="Maximum Brightness 2", + name="Maximum brightness 2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_3, - name="Minimum Brightness 3", + name="Minimum brightness 3", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_3, - name="Maximum Brightness 3", + name="Maximum brightness 3", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), @@ -229,25 +229,25 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "tgq": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, - name="Minimum Brightness", + name="Minimum brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, - name="Maximum Brightness", + name="Maximum brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, - name="Minimum Brightness 2", + name="Minimum brightness 2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, - name="Maximum Brightness 2", + name="Maximum brightness 2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), @@ -265,19 +265,19 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "szjqr": ( NumberEntityDescription( key=DPCode.ARM_DOWN_PERCENT, - name="Move Down %", + name="Move down %", icon="mdi:arrow-down-bold", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.ARM_UP_PERCENT, - name="Move Up %", + name="Move up %", icon="mdi:arrow-up-bold", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLICK_SUSTAIN_TIME, - name="Down Delay", + name="Down delay", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 974268c109f..6ec636b078d 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -57,13 +57,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "kg": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on Behavior", + name="Power on behavior", device_class=TuyaDeviceClass.RELAY_STATUS, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator Light Mode", + name="Indicator light mode", device_class=TuyaDeviceClass.LIGHT_MODE, entity_category=EntityCategory.CONFIG, ), @@ -73,7 +73,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "qn": ( SelectEntityDescription( key=DPCode.LEVEL, - name="Temperature Level", + name="Temperature level", icon="mdi:thermometer-lines", ), ), @@ -96,27 +96,27 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "sp": ( SelectEntityDescription( key=DPCode.IPC_WORK_MODE, - name="IPC Mode", + name="IPC mode", device_class=TuyaDeviceClass.IPC_WORK_MODE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.DECIBEL_SENSITIVITY, - name="Sound Detection Sensitivity", + name="Sound detection densitivity", icon="mdi:volume-vibrate", device_class=TuyaDeviceClass.DECIBEL_SENSITIVITY, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.RECORD_MODE, - name="Record Mode", + name="Record mode", icon="mdi:record-rec", device_class=TuyaDeviceClass.RECORD_MODE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.BASIC_NIGHTVISION, - name="Night Vision", + name="Night vision", icon="mdi:theme-light-dark", device_class=TuyaDeviceClass.BASIC_NIGHTVISION, entity_category=EntityCategory.CONFIG, @@ -130,7 +130,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { ), SelectEntityDescription( key=DPCode.MOTION_SENSITIVITY, - name="Motion Detection Sensitivity", + name="Motion detection sensitivity", icon="mdi:motion-sensor", device_class=TuyaDeviceClass.MOTION_SENSITIVITY, entity_category=EntityCategory.CONFIG, @@ -141,13 +141,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "tdq": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on Behavior", + name="Power on behavior", device_class=TuyaDeviceClass.RELAY_STATUS, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator Light Mode", + name="Indicator light mode", device_class=TuyaDeviceClass.LIGHT_MODE, entity_category=EntityCategory.CONFIG, ), @@ -157,31 +157,31 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "tgkg": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on Behavior", + name="Power on behavior", device_class=TuyaDeviceClass.RELAY_STATUS, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator Light Mode", + name="Indicator light mode", device_class=TuyaDeviceClass.LIGHT_MODE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_1, - name="Light Source Type", + name="Light source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_2, - name="Light 2 Source Type", + name="Light 2 source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_3, - name="Light 3 Source Type", + name="Light 3 source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), @@ -191,13 +191,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "tgq": ( SelectEntityDescription( key=DPCode.LED_TYPE_1, - name="Light Source Type", + name="Light source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_2, - name="Light 2 Source Type", + name="Light 2 source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), @@ -216,14 +216,14 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "sd": ( SelectEntityDescription( key=DPCode.CISTERN, - name="Water Tank Adjustment", + name="Water tank adjustment", entity_category=EntityCategory.CONFIG, device_class=TuyaDeviceClass.VACUUM_CISTERN, icon="mdi:water-opacity", ), SelectEntityDescription( key=DPCode.COLLECTION_MODE, - name="Dust Collection Mode", + name="Dust collection mode", entity_category=EntityCategory.CONFIG, device_class=TuyaDeviceClass.VACUUM_COLLECTION, icon="mdi:air-filter", @@ -241,14 +241,14 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "fs": ( SelectEntityDescription( key=DPCode.FAN_VERTICAL, - name="Vertical Swing Flap Angle", + name="Vertical swing flap angle", device_class=TuyaDeviceClass.FAN_ANGLE, entity_category=EntityCategory.CONFIG, icon="mdi:format-vertical-align-center", ), SelectEntityDescription( key=DPCode.FAN_HORIZONTAL, - name="Horizontal Swing Flap Angle", + name="Horizontal swing flap angle", device_class=TuyaDeviceClass.FAN_ANGLE, entity_category=EntityCategory.CONFIG, icon="mdi:format-horizontal-align-center", @@ -273,7 +273,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "cl": ( SelectEntityDescription( key=DPCode.CONTROL_BACK_MODE, - name="Motor Mode", + name="Motor mode", device_class=TuyaDeviceClass.CURTAIN_MOTOR_MODE, entity_category=EntityCategory.CONFIG, icon="mdi:swap-horizontal", @@ -290,14 +290,14 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "jsq": ( SelectEntityDescription( key=DPCode.SPRAY_MODE, - name="Spray Mode", + name="Spray mode", device_class=TuyaDeviceClass.HUMIDIFIER_SPRAY_MODE, entity_category=EntityCategory.CONFIG, icon="mdi:spray", ), SelectEntityDescription( key=DPCode.LEVEL, - name="Spraying Level", + name="Spraying level", device_class=TuyaDeviceClass.HUMIDIFIER_LEVEL, entity_category=EntityCategory.CONFIG, icon="mdi:spray", diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index dd2996f61ba..266ee951530 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -58,7 +58,7 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( ), TuyaSensorEntityDescription( key=DPCode.BATTERY_STATE, - name="Battery State", + name="Battery state", icon="mdi:battery", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -99,26 +99,26 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO_VALUE, - name="Carbon Monoxide", + name="Carbon monoxide", icon="mdi:molecule-co", device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", icon="mdi:molecule-co2", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, @@ -154,7 +154,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, - name="Smoke Amount", + name="Smoke amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorStateClass.MEASUREMENT, @@ -166,13 +166,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "bh": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Current Temperature", + name="Current temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT_F, - name="Current Temperature", + name="Current temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), @@ -199,7 +199,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -210,7 +210,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cobj": ( TuyaSensorEntityDescription( key=DPCode.CO_VALUE, - name="Carbon Monoxide", + name="Carbon monoxide", device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), @@ -221,7 +221,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cwwsq": ( TuyaSensorEntityDescription( key=DPCode.FEED_REPORT, - name="Last Amount", + name="Last amount", icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, ), @@ -243,7 +243,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -254,13 +254,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -270,19 +270,19 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "jqbj": ( TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -368,7 +368,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -385,7 +385,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "mzj": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Current Temperature", + name="Current temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), @@ -396,7 +396,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.REMAIN_TIME, - name="Remaining Time", + name="Remaining time", native_unit_of_measurement=TIME_MINUTES, icon="mdi:timer", ), @@ -409,7 +409,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "pm2.5": ( TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -420,7 +420,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), @@ -432,7 +432,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -444,13 +444,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PM1, - name="Particulate Matter 1.0 µm", + name="Particulate matter 1.0 µm", device_class=SensorDeviceClass.PM1, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM10, - name="Particulate Matter 10.0 µm", + name="Particulate matter 10.0 µm", device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), @@ -515,13 +515,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "voc": ( TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -544,7 +544,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), @@ -603,7 +603,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "ywbj": ( TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, - name="Smoke Amount", + name="Smoke amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorStateClass.MEASUREMENT, @@ -618,13 +618,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "zndb": ( TuyaSensorEntityDescription( key=DPCode.FORWARD_ENERGY_TOTAL, - name="Total Energy", + name="Total energy", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Current", + name="Phase A current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -632,7 +632,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Power", + name="Phase A power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -640,7 +640,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Voltage", + name="Phase A voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -648,7 +648,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Current", + name="Phase B current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -656,7 +656,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Power", + name="Phase B power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -664,7 +664,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Voltage", + name="Phase B voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -672,7 +672,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Current", + name="Phase C current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -680,7 +680,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Power", + name="Phase C power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -688,7 +688,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Voltage", + name="Phase C voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -700,13 +700,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "dlq": ( TuyaSensorEntityDescription( key=DPCode.TOTAL_FORWARD_ENERGY, - name="Total Energy", + name="Total energy", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Current", + name="Phase A current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -714,7 +714,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Power", + name="Phase A power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -722,7 +722,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Voltage", + name="Phase A voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -730,7 +730,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Current", + name="Phase B current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -738,7 +738,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Power", + name="Phase B power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -746,7 +746,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Voltage", + name="Phase B voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -754,7 +754,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Current", + name="Phase C current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -762,7 +762,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Power", + name="Phase C power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -770,7 +770,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Voltage", + name="Phase C voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -782,13 +782,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "sd": ( TuyaSensorEntityDescription( key=DPCode.CLEAN_AREA, - name="Cleaning Area", + name="Cleaning area", icon="mdi:texture-box", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CLEAN_TIME, - name="Cleaning Time", + name="Cleaning time", icon="mdi:progress-clock", state_class=SensorStateClass.MEASUREMENT, ), @@ -800,37 +800,37 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_TIME, - name="Total Cleaning Time", + name="Total cleaning time", icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_COUNT, - name="Total Cleaning Times", + name="Total cleaning times", icon="mdi:counter", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.DUSTER_CLOTH, - name="Duster Cloth Life", + name="Duster cloth life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.EDGE_BRUSH, - name="Side Brush Life", + name="Side brush life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.FILTER_LIFE, - name="Filter Life", + name="Filter life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ROLL_BRUSH, - name="Rolling Brush Life", + name="Rolling brush life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), @@ -840,7 +840,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cl": ( TuyaSensorEntityDescription( key=DPCode.TIME_TOTAL, - name="Last Operation Duration", + name="Last operation duration", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:progress-clock", ), @@ -868,7 +868,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.LEVEL_CURRENT, - name="Water Level", + name="Water level", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:waves-arrow-up", ), @@ -878,13 +878,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "kj": ( TuyaSensorEntityDescription( key=DPCode.FILTER, - name="Filter Utilization", + name="Filter utilization", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:ticket-percent-outline", ), TuyaSensorEntityDescription( key=DPCode.PM25, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, icon="mdi:molecule", @@ -903,26 +903,26 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.TVOC, - name="Total Volatile Organic Compound", + name="Total volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ECO2, - name="Concentration of Carbon Dioxide", + name="Concentration of carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_TIME, - name="Total Operating Time", + name="Total operating time", icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_PM, - name="Total Absorption of Particles", + name="Total absorption of particles", icon="mdi:texture-box", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, From ba18e11308b11e337ea62b1ff9982796ff640da7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 18:34:27 +0200 Subject: [PATCH 380/820] Remove profiler from mypy ignore list (#74453) --- homeassistant/components/profiler/__init__.py | 12 +++++++----- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 978c19dc2ab..69bbd39a77a 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -7,7 +7,7 @@ import sys import threading import time import traceback -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -123,10 +123,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for thread in threading.enumerate(): if thread == main_thread: continue + ident = cast(int, thread.ident) _LOGGER.critical( "Thread [%s]: %s", thread.name, - "".join(traceback.format_stack(frames.get(thread.ident))).strip(), + "".join(traceback.format_stack(frames.get(ident))).strip(), ) async def _async_dump_scheduled(call: ServiceCall) -> None: @@ -136,13 +137,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: original_maxother = arepr.maxother arepr.maxstring = 300 arepr.maxother = 300 + handle: asyncio.Handle try: - for handle in hass.loop._scheduled: # pylint: disable=protected-access + for handle in getattr(hass.loop, "_scheduled"): if not handle.cancelled(): _LOGGER.critical("Scheduled: %s", handle) finally: - arepr.max_string = original_maxstring - arepr.max_other = original_maxother + arepr.maxstring = original_maxstring + arepr.maxother = original_maxother async_register_admin_service( hass, diff --git a/mypy.ini b/mypy.ini index cccfdc55091..3cdd8b2880f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2719,9 +2719,6 @@ ignore_errors = true [mypy-homeassistant.components.plex.media_player] ignore_errors = true -[mypy-homeassistant.components.profiler] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 8ddfa2b53a4..76304419c6c 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -37,7 +37,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.device", "homeassistant.components.onvif.sensor", "homeassistant.components.plex.media_player", - "homeassistant.components.profiler", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 7c2bd319f1773004d59129ea9a21ca23a24f61df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:59:21 +0200 Subject: [PATCH 381/820] Migrate AdGuard Home to new entity naming style (#74999) * Migrate AdGuard Home to new entity naming style * sentence-casing --- homeassistant/components/adguard/__init__.py | 2 ++ homeassistant/components/adguard/sensor.py | 16 ++++++++-------- homeassistant/components/adguard/switch.py | 16 ++++++---------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 2bb15d19223..0c43f84bdfc 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -132,6 +132,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class AdGuardHomeEntity(Entity): """Defines a base AdGuard Home entity.""" + _attr_has_entity_name = True + def __init__( self, adguard: AdGuardHome, diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 8134d2c4d43..2d3226aba59 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -100,7 +100,7 @@ class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard DNS Queries", + "DNS queries", "mdi:magnify", "dns_queries", "queries", @@ -119,7 +119,7 @@ class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard DNS Queries Blocked", + "DNS queries blocked", "mdi:magnify-close", "blocked_filtering", "queries", @@ -139,7 +139,7 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard DNS Queries Blocked Ratio", + "DNS queries blocked ratio", "mdi:magnify-close", "blocked_percentage", PERCENTAGE, @@ -159,7 +159,7 @@ class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Parental Control Blocked", + "Parental control blocked", "mdi:human-male-girl", "blocked_parental", "requests", @@ -178,7 +178,7 @@ class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Safe Browsing Blocked", + "Safe browsing blocked", "mdi:shield-half-full", "blocked_safebrowsing", "requests", @@ -197,7 +197,7 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Safe Searches Enforced", + "Safe searches enforced", "mdi:shield-search", "enforced_safesearch", "requests", @@ -216,7 +216,7 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Average Processing Speed", + "Average processing speed", "mdi:speedometer", "average_speed", TIME_MILLISECONDS, @@ -236,7 +236,7 @@ class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Rules Count", + "Rules count", "mdi:counter", "rules_count", "rules", diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 0cb7a48fee6..5b4017d4054 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -107,9 +107,7 @@ class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "AdGuard Protection", "mdi:shield-check", "protection" - ) + super().__init__(adguard, entry, "Protection", "mdi:shield-check", "protection") async def _adguard_turn_off(self) -> None: """Turn off the switch.""" @@ -130,7 +128,7 @@ class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, entry, "AdGuard Parental Control", "mdi:shield-check", "parental" + adguard, entry, "Parental control", "mdi:shield-check", "parental" ) async def _adguard_turn_off(self) -> None: @@ -152,7 +150,7 @@ class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, entry, "AdGuard Safe Search", "mdi:shield-check", "safesearch" + adguard, entry, "Safe search", "mdi:shield-check", "safesearch" ) async def _adguard_turn_off(self) -> None: @@ -174,7 +172,7 @@ class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, entry, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" + adguard, entry, "Safe browsing", "mdi:shield-check", "safebrowsing" ) async def _adguard_turn_off(self) -> None: @@ -195,9 +193,7 @@ class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "AdGuard Filtering", "mdi:shield-check", "filtering" - ) + super().__init__(adguard, entry, "Filtering", "mdi:shield-check", "filtering") async def _adguard_turn_off(self) -> None: """Turn off the switch.""" @@ -220,7 +216,7 @@ class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): super().__init__( adguard, entry, - "AdGuard Query Log", + "Query log", "mdi:shield-check", "querylog", enabled_default=False, From 28a34a1f8975a41241060c5fffef4b9a7618a3cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 19:14:23 +0200 Subject: [PATCH 382/820] Remove withings from mypy ignore list (#74966) --- homeassistant/components/withings/__init__.py | 5 +++-- homeassistant/components/withings/binary_sensor.py | 2 +- homeassistant/components/withings/common.py | 8 ++++---- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 5 files changed, 8 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 1b77faa7a61..da2174c3822 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -6,6 +6,7 @@ For more details about this platform, please refer to the documentation at from __future__ import annotations import asyncio +from typing import Any from aiohttp.web import Request, Response import voluptuous as vol @@ -103,7 +104,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Withings from a config entry.""" - config_updates = {} + config_updates: dict[str, Any] = {} # Add a unique id if it's an older config entry. if entry.unique_id != entry.data["token"]["userid"] or not isinstance( @@ -197,7 +198,7 @@ async def async_webhook_handler( return json_message_response("Parameter appli not provided", message_code=20) try: - appli = NotifyAppli(int(params.getone("appli"))) + appli = NotifyAppli(int(params.getone("appli"))) # type: ignore[arg-type] except ValueError: return json_message_response("Invalid appli provided", message_code=21) diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index b0af5051124..ff98e6e0d45 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -32,6 +32,6 @@ class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.OCCUPANCY @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self._state_data diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 90f60cd4112..93c3800e42f 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -10,7 +10,7 @@ from enum import Enum, IntEnum from http import HTTPStatus import logging import re -from typing import Any +from typing import Any, Union from aiohttp.web import Response import requests @@ -589,7 +589,7 @@ class DataManager: update_method=self.async_subscribe_webhook, ) self.poll_data_update_coordinator = DataUpdateCoordinator[ - dict[MeasureType, Any] + Union[dict[MeasureType, Any], None] ]( hass, _LOGGER, @@ -951,7 +951,7 @@ class BaseWithingsSensor(Entity): return self._unique_id @property - def icon(self) -> str: + def icon(self) -> str | None: """Icon to use in the frontend, if any.""" return self._attribute.icon @@ -1005,7 +1005,7 @@ async def async_get_data_manager( config_entry_data = hass.data[const.DOMAIN][config_entry.entry_id] if const.DATA_MANAGER not in config_entry_data: - profile = config_entry.data.get(const.PROFILE) + profile: str = config_entry.data[const.PROFILE] _LOGGER.debug("Creating withings data manager for profile: %s", profile) config_entry_data[const.DATA_MANAGER] = DataManager( diff --git a/mypy.ini b/mypy.ini index 3cdd8b2880f..17887f7c78c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2760,15 +2760,3 @@ ignore_errors = true [mypy-homeassistant.components.template.sensor] ignore_errors = true - -[mypy-homeassistant.components.withings] -ignore_errors = true - -[mypy-homeassistant.components.withings.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.withings.common] -ignore_errors = true - -[mypy-homeassistant.components.withings.config_flow] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 76304419c6c..e1b2d60ddd1 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -51,10 +51,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.statistics", "homeassistant.components.template.number", "homeassistant.components.template.sensor", - "homeassistant.components.withings", - "homeassistant.components.withings.binary_sensor", - "homeassistant.components.withings.common", - "homeassistant.components.withings.config_flow", ] # Component modules which should set no_implicit_reexport = true. From 14baaf4b67beb188a8d7e091c20aef274d7610a2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 20:02:44 +0200 Subject: [PATCH 383/820] Remove plex from mypy ignore list (#74984) --- homeassistant/components/plex/media_player.py | 40 +++++++++---------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index ce76c4be3ff..7d21fff3afe 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -248,7 +248,7 @@ class PlexMediaPlayer(MediaPlayerEntity): else: self._attr_state = STATE_IDLE - @property + @property # type: ignore[misc] @needs_session def username(self): """Return the username of the client owner.""" @@ -279,109 +279,109 @@ class PlexMediaPlayer(MediaPlayerEntity): return "video" - @property + @property # type: ignore[misc] @needs_session def session_key(self): """Return current session key.""" return self.session.sessionKey - @property + @property # type: ignore[misc] @needs_session def media_library_title(self): """Return the library name of playing media.""" return self.session.media_library_title - @property + @property # type: ignore[misc] @needs_session def media_content_id(self): """Return the content ID of current playing media.""" return self.session.media_content_id - @property + @property # type: ignore[misc] @needs_session def media_content_type(self): """Return the content type of current playing media.""" return self.session.media_content_type - @property + @property # type: ignore[misc] @needs_session def media_content_rating(self): """Return the content rating of current playing media.""" return self.session.media_content_rating - @property + @property # type: ignore[misc] @needs_session def media_artist(self): """Return the artist of current playing media, music track only.""" return self.session.media_artist - @property + @property # type: ignore[misc] @needs_session def media_album_name(self): """Return the album name of current playing media, music track only.""" return self.session.media_album_name - @property + @property # type: ignore[misc] @needs_session def media_album_artist(self): """Return the album artist of current playing media, music only.""" return self.session.media_album_artist - @property + @property # type: ignore[misc] @needs_session def media_track(self): """Return the track number of current playing media, music only.""" return self.session.media_track - @property + @property # type: ignore[misc] @needs_session def media_duration(self): """Return the duration of current playing media in seconds.""" return self.session.media_duration - @property + @property # type: ignore[misc] @needs_session def media_position(self): """Return the duration of current playing media in seconds.""" return self.session.media_position - @property + @property # type: ignore[misc] @needs_session def media_position_updated_at(self): """When was the position of the current playing media valid.""" return self.session.media_position_updated_at - @property + @property # type: ignore[misc] @needs_session def media_image_url(self): """Return the image URL of current playing media.""" return self.session.media_image_url - @property + @property # type: ignore[misc] @needs_session def media_summary(self): """Return the summary of current playing media.""" return self.session.media_summary - @property + @property # type: ignore[misc] @needs_session def media_title(self): """Return the title of current playing media.""" return self.session.media_title - @property + @property # type: ignore[misc] @needs_session def media_season(self): """Return the season of current playing media (TV Show only).""" return self.session.media_season - @property + @property # type: ignore[misc] @needs_session def media_series_title(self): """Return the title of the series of current playing media.""" return self.session.media_series_title - @property + @property # type: ignore[misc] @needs_session def media_episode(self): """Return the episode of current playing media (TV Show only).""" @@ -515,7 +515,7 @@ class PlexMediaPlayer(MediaPlayerEntity): return attributes @property - def device_info(self) -> DeviceInfo: + def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" if self.machine_identifier is None: return None diff --git a/mypy.ini b/mypy.ini index 17887f7c78c..b973b959213 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2716,9 +2716,6 @@ ignore_errors = true [mypy-homeassistant.components.onvif.sensor] ignore_errors = true -[mypy-homeassistant.components.plex.media_player] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e1b2d60ddd1..87ace51a3d6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -36,7 +36,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.camera", "homeassistant.components.onvif.device", "homeassistant.components.onvif.sensor", - "homeassistant.components.plex.media_player", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 2986a2f01b6dba6ac67c005bb7e07d753007842e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 11 Jul 2022 14:19:30 -0400 Subject: [PATCH 384/820] Identify the active ZHA coordinator device in API responses (#74739) * Remove deprecated zigpy properties * Create a `ZHADevice.is_active_coordinator` property * Add `@puddly` to the ZHA code owners * Create a `ZHAGateway.coordinator_ieee` shortcut property --- CODEOWNERS | 4 +-- homeassistant/components/zha/__init__.py | 6 ++-- homeassistant/components/zha/api.py | 5 +-- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/core/device.py | 12 ++++++- homeassistant/components/zha/core/gateway.py | 11 ++++--- homeassistant/components/zha/manifest.json | 2 +- tests/components/zha/test_device.py | 34 ++++++++++++++++++-- 8 files changed, 57 insertions(+), 18 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a473d07af9d..d2ad6ef2307 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1238,8 +1238,8 @@ build.json @home-assistant/supervisor /tests/components/zeroconf/ @bdraco /homeassistant/components/zerproc/ @emlove /tests/components/zerproc/ @emlove -/homeassistant/components/zha/ @dmulcahey @adminiuga -/tests/components/zha/ @dmulcahey @adminiuga +/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly +/tests/components/zha/ @dmulcahey @adminiuga @puddly /homeassistant/components/zodiac/ @JulienTant /tests/components/zodiac/ @JulienTant /homeassistant/components/zone/ @home-assistant/core diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 80956117ff9..7fd9495635f 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -119,10 +119,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={ - (dr.CONNECTION_ZIGBEE, str(zha_gateway.application_controller.ieee)) - }, - identifiers={(DOMAIN, str(zha_gateway.application_controller.ieee))}, + connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.coordinator_ieee))}, + identifiers={(DOMAIN, str(zha_gateway.coordinator_ieee))}, name="Zigbee Coordinator", manufacturer="ZHA", model=zha_gateway.radio_description, diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index cc4dd45689e..89d360577d4 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1091,10 +1091,7 @@ def async_load_api(hass: HomeAssistant) -> None: zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = service.data[ATTR_IEEE] zha_device: ZHADevice | None = zha_gateway.get_device(ieee) - if zha_device is not None and ( - zha_device.is_coordinator - and zha_device.ieee == zha_gateway.application_controller.ieee - ): + if zha_device is not None and zha_device.is_active_coordinator: _LOGGER.info("Removing the coordinator (%s) is not allowed", ieee) return _LOGGER.info("Removing node %s", ieee) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c2d9e926453..0d6dec2d816 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -17,6 +17,7 @@ import zigpy_znp.zigbee.application from homeassistant.const import Platform import homeassistant.helpers.config_validation as cv +ATTR_ACTIVE_COORDINATOR = "active_coordinator" ATTR_ARGS = "args" ATTR_ATTRIBUTE = "attribute" ATTR_ATTRIBUTE_ID = "attribute_id" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index afd4f647ecb..4719b6bf585 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -30,6 +30,7 @@ from homeassistant.helpers.event import async_track_time_interval from . import channels from .const import ( + ATTR_ACTIVE_COORDINATOR, ATTR_ARGS, ATTR_ATTRIBUTE, ATTR_AVAILABLE, @@ -252,12 +253,20 @@ class ZHADevice(LogMixin): @property def is_coordinator(self) -> bool | None: - """Return true if this device represents the coordinator.""" + """Return true if this device represents a coordinator.""" if self._zigpy_device.node_desc is None: return None return self._zigpy_device.node_desc.is_coordinator + @property + def is_active_coordinator(self) -> bool: + """Return true if this device is the active coordinator.""" + if not self.is_coordinator: + return False + + return self.ieee == self.gateway.coordinator_ieee + @property def is_end_device(self) -> bool | None: """Return true if this device is an end device.""" @@ -499,6 +508,7 @@ class ZHADevice(LogMixin): """Get ZHA device information.""" device_info: dict[str, Any] = {} device_info.update(self.device_info) + device_info[ATTR_ACTIVE_COORDINATOR] = self.is_active_coordinator device_info["entities"] = [ { "entity_id": entity_ref.reference_id, diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 7782d8ef7fd..9efb6e99550 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -178,9 +178,7 @@ class ZHAGateway: self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self - self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str( - self.application_controller.ieee - ) + self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) self.async_load_devices() self.async_load_groups() @@ -189,7 +187,7 @@ class ZHAGateway: """Restore ZHA devices from zigpy application state.""" for zigpy_device in self.application_controller.devices.values(): zha_device = self._async_get_or_create_device(zigpy_device, restored=True) - if zha_device.ieee == self.application_controller.ieee: + if zha_device.ieee == self.coordinator_ieee: self.coordinator_zha_device = zha_device delta_msg = "not known" if zha_device.last_seen is not None: @@ -435,6 +433,11 @@ class ZHAGateway: ) self.ha_entity_registry.async_remove(entry.entity_id) + @property + def coordinator_ieee(self) -> EUI64: + """Return the active coordinator's IEEE address.""" + return self.application_controller.state.node_info.ieee + @property def devices(self) -> dict[EUI64, ZHADevice]: """Return devices.""" diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 78aea7c9fb6..c97cca8deb3 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -76,7 +76,7 @@ "known_devices": ["Bitron Video AV2010/10"] } ], - "codeowners": ["@dmulcahey", "@adminiuga"], + "codeowners": ["@dmulcahey", "@adminiuga", "@puddly"], "zeroconf": [ { "type": "_esphomelib._tcp.local.", diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index f05a5cd1872..733a8e99e4b 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -6,7 +6,9 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha +import zigpy.types import zigpy.zcl.clusters.general as general +import zigpy.zdo.types as zdo_t from homeassistant.components.zha.core.const import ( CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, @@ -42,7 +44,7 @@ def required_platforms_only(): def zigpy_device(zigpy_device_mock): """Device tracker zigpy device.""" - def _dev(with_basic_channel: bool = True): + def _dev(with_basic_channel: bool = True, **kwargs): in_clusters = [general.OnOff.cluster_id] if with_basic_channel: in_clusters.append(general.Basic.cluster_id) @@ -54,7 +56,7 @@ def zigpy_device(zigpy_device_mock): SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, } } - return zigpy_device_mock(endpoints) + return zigpy_device_mock(endpoints, **kwargs) return _dev @@ -321,3 +323,31 @@ async def test_device_restore_availability( assert hass.states.get(entity_id).state == STATE_OFF else: assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_device_is_active_coordinator(hass, zha_device_joined, zigpy_device): + """Test that the current coordinator is uniquely detected.""" + + current_coord_dev = zigpy_device(ieee="aa:bb:cc:dd:ee:ff:00:11", nwk=0x0000) + current_coord_dev.node_desc = current_coord_dev.node_desc.replace( + logical_type=zdo_t.LogicalType.Coordinator + ) + + old_coord_dev = zigpy_device(ieee="aa:bb:cc:dd:ee:ff:00:12", nwk=0x0000) + old_coord_dev.node_desc = old_coord_dev.node_desc.replace( + logical_type=zdo_t.LogicalType.Coordinator + ) + + # The two coordinators have different IEEE addresses + assert current_coord_dev.ieee != old_coord_dev.ieee + + current_coordinator = await zha_device_joined(current_coord_dev) + stale_coordinator = await zha_device_joined(old_coord_dev) + + # Ensure the current ApplicationController's IEEE matches our coordinator's + current_coordinator.gateway.application_controller.state.node_info.ieee = ( + current_coord_dev.ieee + ) + + assert current_coordinator.is_active_coordinator + assert not stale_coordinator.is_active_coordinator From 5774f2e7b96e8280efd36bf4d20bed87a05cc8c3 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 11 Jul 2022 14:29:42 -0400 Subject: [PATCH 385/820] Use forward_entry_setups in ZHA (#74834) --- homeassistant/components/zha/__init__.py | 23 ++++------------------ homeassistant/components/zha/core/const.py | 1 - 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 7fd9495635f..70b9dfd9b46 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -31,7 +31,6 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_GATEWAY, - DATA_ZHA_PLATFORM_LOADED, DATA_ZHA_SHUTDOWN_TASK, DOMAIN, PLATFORMS, @@ -111,11 +110,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() - zha_data[DATA_ZHA_PLATFORM_LOADED] = [] - for platform in PLATFORMS: - coro = hass.config_entries.async_forward_entry_setup(config_entry, platform) - zha_data[DATA_ZHA_PLATFORM_LOADED].append(hass.async_create_task(coro)) - device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -136,7 +130,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown ) - asyncio.create_task(async_load_entities(hass)) + + await zha_gateway.async_initialize_devices_and_entities() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) return True @@ -161,18 +158,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def async_load_entities(hass: HomeAssistant) -> None: - """Load entities after integration was setup.""" - zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - await zha_gateway.async_initialize_devices_and_entities() - to_setup = hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED] - results = await asyncio.gather(*to_setup, return_exceptions=True) - for res in results: - if isinstance(res, Exception): - _LOGGER.warning("Couldn't setup zha platform: %s", res) - async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) - - async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 0d6dec2d816..de373215e52 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -171,7 +171,6 @@ DATA_ZHA_CONFIG = "config" DATA_ZHA_BRIDGE_ID = "zha_bridge_id" DATA_ZHA_CORE_EVENTS = "zha_core_events" DATA_ZHA_GATEWAY = "zha_gateway" -DATA_ZHA_PLATFORM_LOADED = "platform_loaded" DATA_ZHA_SHUTDOWN_TASK = "zha_shutdown_task" DEBUG_COMP_BELLOWS = "bellows" From 2d2fd3e48fd626290e0f98956623bd0e483b4742 Mon Sep 17 00:00:00 2001 From: 0bmay <57501269+0bmay@users.noreply.github.com> Date: Mon, 11 Jul 2022 12:53:33 -0700 Subject: [PATCH 386/820] Cache Canary camera image (#73923) * Cache camera image Cache camera image so a new image isn't generated each call. Adds debug logging * Apply suggestions from code review code compression with walrus operator Co-authored-by: Erik Montnemery * fix after walrus operator suggested tweak fully use the live_stream_session variable in async_camera_image * Invalidate cached image after 15 minutes requested code change; invalidate cached image * Removed unnecessary if statement based on code review * Image capture flow updates now sets the image expiration upon getting an updated image updates the cache image only when a new image is captured Co-authored-by: Erik Montnemery --- homeassistant/components/canary/camera.py | 57 +++++++++++++++++------ 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 44bac9e3bde..04d8d159541 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +import logging from typing import Final from aiohttp.web import Request, StreamResponse @@ -23,7 +24,7 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import Throttle +from homeassistant.util import dt as dt_util from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -34,7 +35,7 @@ from .const import ( ) from .coordinator import CanaryDataUpdateCoordinator -MIN_TIME_BETWEEN_SESSION_RENEW: Final = timedelta(seconds=90) +FORCE_CAMERA_REFRESH_INTERVAL: Final = timedelta(minutes=15) PLATFORM_SCHEMA: Final = vol.All( cv.deprecated(CONF_FFMPEG_ARGUMENTS), @@ -47,6 +48,8 @@ PLATFORM_SCHEMA: Final = vol.All( ), ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -105,6 +108,11 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): model=device.device_type["name"], name=device.name, ) + self._image: bytes | None = None + self._expires_at = dt_util.utcnow() + _LOGGER.debug( + "%s %s has been initialized", self.name, device.device_type["name"] + ) @property def location(self) -> Location: @@ -125,17 +133,33 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - await self.hass.async_add_executor_job(self.renew_live_stream_session) - live_stream_url = await self.hass.async_add_executor_job( - getattr, self._live_stream_session, "live_stream_url" - ) - return await ffmpeg.async_get_image( - self.hass, - live_stream_url, - extra_cmd=self._ffmpeg_arguments, - width=width, - height=height, - ) + utcnow = dt_util.utcnow() + if self._expires_at <= utcnow: + _LOGGER.debug("Grabbing a live view image from %s", self.name) + await self.hass.async_add_executor_job(self.renew_live_stream_session) + + if (live_stream_session := self._live_stream_session) is None: + return None + + if not (live_stream_url := live_stream_session.live_stream_url): + return None + + image = await ffmpeg.async_get_image( + self.hass, + live_stream_url, + extra_cmd=self._ffmpeg_arguments, + width=width, + height=height, + ) + + if image: + self._image = image + self._expires_at = FORCE_CAMERA_REFRESH_INTERVAL + utcnow + _LOGGER.debug("Grabbed a live view image from %s", self.name) + await self.hass.async_add_executor_job(live_stream_session.stop_session) + _LOGGER.debug("Stopped live session from %s", self.name) + + return self._image async def handle_async_mjpeg_stream( self, request: Request @@ -161,9 +185,14 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): finally: await stream.close() - @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) def renew_live_stream_session(self) -> None: """Renew live stream session.""" self._live_stream_session = self.coordinator.canary.get_live_stream_session( self._device ) + + _LOGGER.debug( + "Live Stream URL for %s is %s", + self.name, + self._live_stream_session.live_stream_url, + ) From ef025bccc02584e59620404d73d0ce5251d753d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 23:30:48 +0200 Subject: [PATCH 387/820] Update tqdm to 4.64.0 (#75010) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index fa0dcb68a09..1ca1fc05819 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -28,7 +28,7 @@ requests_mock==1.9.2 respx==0.19.2 stdlib-list==0.7.0 tomli==2.0.1;python_version<"3.11" -tqdm==4.49.0 +tqdm==4.64.0 types-atomicwrites==1.4.1 types-croniter==1.0.0 types-backports==0.1.3 From da027fa3905d36b91beefd456cb546f891617eab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jul 2022 23:46:55 +0200 Subject: [PATCH 388/820] JSON serialize NamedTuple subclasses with aiohttp (#74971) --- homeassistant/helpers/aiohttp_client.py | 4 ++-- tests/helpers/test_aiohttp_client.py | 11 +++++++++++ tests/helpers/test_json.py | 22 ++++++++++++++++++++++ tests/test_util/aiohttp.py | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2e56698db41..2ef96091d15 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -14,7 +14,6 @@ from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout -import orjson from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ @@ -23,6 +22,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util from .frame import warn_use +from .json import json_dumps DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" @@ -98,7 +98,7 @@ def _async_create_clientsession( """Create a new ClientSession with kwargs, i.e. for cookies.""" clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), - json_serialize=lambda x: orjson.dumps(x).decode("utf-8"), + json_serialize=json_dumps, **kwargs, ) # Prevent packages accidentally overriding our default headers diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 599b2c6984e..1ffb4267167 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE import homeassistant.helpers.aiohttp_client as client +from homeassistant.util.color import RGBColor from tests.common import MockConfigEntry @@ -215,6 +216,16 @@ async def test_async_aiohttp_proxy_stream_client_err(aioclient_mock, camera_clie assert resp.status == 502 +async def test_sending_named_tuple(hass, aioclient_mock): + """Test sending a named tuple in json.""" + resp = aioclient_mock.post("http://127.0.0.1/rgb", json={"rgb": RGBColor(4, 3, 2)}) + session = client.async_create_clientsession(hass) + resp = await session.post("http://127.0.0.1/rgb", json={"rgb": RGBColor(4, 3, 2)}) + assert resp.status == 200 + await resp.json() == {"rgb": RGBColor(4, 3, 2)} + aioclient_mock.mock_calls[0][2]["rgb"] == RGBColor(4, 3, 2) + + async def test_client_session_immutable_headers(hass): """Test we can't mutate headers.""" session = client.async_get_clientsession(hass) diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 54c488690fa..1e85338f152 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -2,6 +2,7 @@ import datetime import json import time +from typing import NamedTuple import pytest @@ -13,6 +14,7 @@ from homeassistant.helpers.json import ( json_dumps_sorted, ) from homeassistant.util import dt as dt_util +from homeassistant.util.color import RGBColor @pytest.mark.parametrize("encoder", (JSONEncoder, ExtendedJSONEncoder)) @@ -96,3 +98,23 @@ def test_json_dumps_tuple_subclass(): tt = time.struct_time((1999, 3, 17, 32, 44, 55, 2, 76, 0)) assert json_dumps(tt) == "[1999,3,17,32,44,55,2,76,0]" + + +def test_json_dumps_named_tuple_subclass(): + """Test the json dumps a tuple subclass.""" + + class NamedTupleSubclass(NamedTuple): + """A NamedTuple subclass.""" + + name: str + + nts = NamedTupleSubclass("a") + + assert json_dumps(nts) == '["a"]' + + +def test_json_dumps_rgb_color_subclass(): + """Test the json dumps of RGBColor.""" + rgb = RGBColor(4, 2, 1) + + assert json_dumps(rgb) == "[4,2,1]" diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 9ed47109210..4ed81a3a577 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -111,7 +111,7 @@ class AiohttpClientMocker: def create_session(self, loop): """Create a ClientSession that is bound to this mocker.""" - session = ClientSession(loop=loop) + session = ClientSession(loop=loop, json_serialize=json_dumps) # Setting directly on `session` will raise deprecation warning object.__setattr__(session, "_request", self.match_request) return session From 2e228b2608c8e6c5ed2da932f85aa48fcdfef8dd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Jul 2022 00:10:53 +0200 Subject: [PATCH 389/820] Tweak handling of entities with `has_entity_name` set (#74948) Co-authored-by: Paulus Schoutsen --- .../components/config/entity_registry.py | 10 +++-- homeassistant/helpers/entity_platform.py | 1 + .../components/config/test_entity_registry.py | 42 +++++++++++-------- tests/helpers/test_entity_platform.py | 2 + 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 445ca96c8b0..6d022aa2d14 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,6 +1,8 @@ """HTTP views to interact with the entity registry.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -223,32 +225,32 @@ def websocket_remove_entity(hass, connection, msg): @callback -def _entry_dict(entry): +def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: """Convert entry to API format.""" return { "area_id": entry.area_id, "config_entry_id": entry.config_entry_id, "device_id": entry.device_id, "disabled_by": entry.disabled_by, + "has_entity_name": entry.has_entity_name, "entity_category": entry.entity_category, "entity_id": entry.entity_id, "hidden_by": entry.hidden_by, "icon": entry.icon, "name": entry.name, + "original_name": entry.original_name, "platform": entry.platform, } @callback -def _entry_ext_dict(entry): +def _entry_ext_dict(entry: er.RegistryEntry) -> dict[str, Any]: """Convert entry to API format.""" data = _entry_dict(entry) data["capabilities"] = entry.capabilities data["device_class"] = entry.device_class - data["has_entity_name"] = entry.has_entity_name data["options"] = entry.options data["original_device_class"] = entry.original_device_class data["original_icon"] = entry.original_icon - data["original_name"] = entry.original_name data["unique_id"] = entry.unique_id return data diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6253b939bed..2049565e859 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -544,6 +544,7 @@ class EntityPlatform: entity_category=entity.entity_category, hidden_by=hidden_by, known_object_ids=self.entities.keys(), + has_entity_name=entity.has_entity_name, original_device_class=entity.device_class, original_icon=entity.icon, original_name=entity.name, diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 9c5984f751e..e472736ee6c 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -57,28 +57,32 @@ async def test_list_entities(hass, client): assert msg["result"] == [ { + "area_id": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "entity_id": "test_domain.name", - "hidden_by": None, - "name": "Hello World", - "icon": None, - "platform": "test_platform", "entity_category": None, + "entity_id": "test_domain.name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "name": "Hello World", + "original_name": None, + "platform": "test_platform", }, { + "area_id": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "entity_id": "test_domain.no_name", - "hidden_by": None, - "name": None, - "icon": None, - "platform": "test_platform", "entity_category": None, + "entity_id": "test_domain.no_name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "name": None, + "original_name": None, + "platform": "test_platform", }, ] @@ -103,16 +107,18 @@ async def test_list_entities(hass, client): assert msg["result"] == [ { + "area_id": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "entity_id": "test_domain.name", - "hidden_by": None, - "name": "Hello World", - "icon": None, - "platform": "test_platform", "entity_category": None, + "entity_id": "test_domain.name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "name": "Hello World", + "original_name": None, + "platform": "test_platform", }, ] diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 80a37f9f2fd..d7f77eeacda 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1194,6 +1194,7 @@ async def test_entity_info_added_to_entity_registry(hass): capability_attributes={"max": 100}, device_class="mock-device-class", entity_category=EntityCategory.CONFIG, + has_entity_name=True, icon="nice:icon", name="best name", supported_features=5, @@ -1213,6 +1214,7 @@ async def test_entity_info_added_to_entity_registry(hass): capabilities={"max": 100}, device_class=None, entity_category=EntityCategory.CONFIG, + has_entity_name=True, icon=None, id=ANY, name=None, From 7b9a0eed22f01348df5a6c87bbddc4091f513746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 12 Jul 2022 01:35:45 +0300 Subject: [PATCH 390/820] Upgrade huawei-lte-api to 1.6.1 (#75030) --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 656c80e5a89..910e0e132f1 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "huawei-lte-api==1.6.0", + "huawei-lte-api==1.6.1", "stringcase==1.2.0", "url-normalize==1.4.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 5376d91ed2e..933ca47b086 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -849,7 +849,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.0 +huawei-lte-api==1.6.1 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ad2dd3957f..0d94346b384 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.0 +huawei-lte-api==1.6.1 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 From 5d8e1b8387a9d17f9b55dcea2a816c303904701a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 12 Jul 2022 00:24:26 +0000 Subject: [PATCH 391/820] [ci skip] Translation update --- .../components/ambee/translations/pt.json | 11 +++++++++++ .../components/binary_sensor/translations/pt.json | 6 ++++++ .../climacell/translations/sensor.pt.json | 7 +++++++ .../components/deconz/translations/pt.json | 1 + homeassistant/components/demo/translations/pt.json | 1 + .../components/derivative/translations/pt.json | 11 +++++++++++ .../components/econet/translations/pt.json | 7 +++++++ .../evil_genius_labs/translations/pt.json | 11 +++++++++++ .../components/ezviz/translations/pt.json | 11 +++++++++++ .../components/flipr/translations/pt.json | 7 +++++++ .../components/foscam/translations/pt.json | 3 +++ .../components/fritz/translations/pt.json | 8 ++++++++ .../components/goodwe/translations/pt.json | 7 +++++++ .../components/group/translations/pt.json | 9 +++++++++ .../components/harmony/translations/pt.json | 9 +++++++++ .../here_travel_time/translations/pl.json | 7 +++++++ .../here_travel_time/translations/pt.json | 9 +++++++++ homeassistant/components/hue/translations/pt.json | 6 ++++++ .../components/insteon/translations/pt.json | 6 ++++++ .../components/integration/translations/pt.json | 11 +++++++++++ .../components/iotawatt/translations/pt.json | 11 +++++++++++ .../components/isy994/translations/pt.json | 5 +++++ homeassistant/components/knx/translations/pt.json | 11 +++++++++++ .../components/meteoclimatic/translations/pt.json | 7 +++++++ .../components/nextdns/translations/pt.json | 11 +++++++++++ homeassistant/components/nina/translations/pt.json | 9 +++++++++ .../components/nmap_tracker/translations/pt.json | 11 +++++++++++ .../components/nuheat/translations/pt.json | 1 + .../components/overkiz/translations/sensor.pt.json | 7 +++++++ homeassistant/components/peco/translations/pt.json | 11 +++++++++++ .../components/plaato/translations/pt.json | 1 + .../components/rachio/translations/pt.json | 9 +++++++++ .../rainforest_eagle/translations/pt.json | 11 +++++++++++ .../components/rhasspy/translations/et.json | 12 ++++++++++++ .../components/rhasspy/translations/fr.json | 12 ++++++++++++ .../components/rhasspy/translations/pt-BR.json | 12 ++++++++++++ .../components/roomba/translations/pt.json | 5 +++++ .../components/select/translations/pt.json | 7 +++++++ .../components/shelly/translations/pt.json | 5 +++++ .../components/squeezebox/translations/pt.json | 3 ++- .../components/tankerkoenig/translations/pt.json | 11 +++++++++++ .../tomorrowio/translations/sensor.pt.json | 7 +++++++ .../components/tuya/translations/select.pt.json | 14 ++++++++++++++ .../components/tuya/translations/sensor.pt.json | 7 +++++++ homeassistant/components/upb/translations/pt.json | 7 +++++++ .../components/watttime/translations/pt.json | 11 +++++++++++ .../components/withings/translations/af.json | 9 +++++++++ .../components/withings/translations/ar.json | 9 +++++++++ .../components/withings/translations/bg.json | 3 +++ .../components/withings/translations/bn.json | 9 +++++++++ .../components/withings/translations/bs.json | 9 +++++++++ .../components/withings/translations/ca.json | 3 +++ .../components/withings/translations/de.json | 4 ++++ .../components/withings/translations/en.json | 4 ++++ .../components/withings/translations/et.json | 4 ++++ .../components/withings/translations/eu.json | 9 +++++++++ .../components/withings/translations/fr.json | 6 +++++- .../components/withings/translations/hy.json | 9 +++++++++ .../components/withings/translations/pl.json | 4 ++++ .../components/withings/translations/pt-BR.json | 4 ++++ .../components/withings/translations/zh-Hans.json | 9 +++++++++ .../components/withings/translations/zh-Hant.json | 4 ++++ homeassistant/components/wled/translations/pt.json | 3 +++ .../xiaomi_miio/translations/select.pt.json | 7 +++++++ 64 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/ambee/translations/pt.json create mode 100644 homeassistant/components/climacell/translations/sensor.pt.json create mode 100644 homeassistant/components/derivative/translations/pt.json create mode 100644 homeassistant/components/econet/translations/pt.json create mode 100644 homeassistant/components/evil_genius_labs/translations/pt.json create mode 100644 homeassistant/components/ezviz/translations/pt.json create mode 100644 homeassistant/components/flipr/translations/pt.json create mode 100644 homeassistant/components/fritz/translations/pt.json create mode 100644 homeassistant/components/goodwe/translations/pt.json create mode 100644 homeassistant/components/integration/translations/pt.json create mode 100644 homeassistant/components/iotawatt/translations/pt.json create mode 100644 homeassistant/components/knx/translations/pt.json create mode 100644 homeassistant/components/meteoclimatic/translations/pt.json create mode 100644 homeassistant/components/nextdns/translations/pt.json create mode 100644 homeassistant/components/nina/translations/pt.json create mode 100644 homeassistant/components/nmap_tracker/translations/pt.json create mode 100644 homeassistant/components/overkiz/translations/sensor.pt.json create mode 100644 homeassistant/components/peco/translations/pt.json create mode 100644 homeassistant/components/rainforest_eagle/translations/pt.json create mode 100644 homeassistant/components/rhasspy/translations/et.json create mode 100644 homeassistant/components/rhasspy/translations/fr.json create mode 100644 homeassistant/components/rhasspy/translations/pt-BR.json create mode 100644 homeassistant/components/select/translations/pt.json create mode 100644 homeassistant/components/tankerkoenig/translations/pt.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.pt.json create mode 100644 homeassistant/components/tuya/translations/select.pt.json create mode 100644 homeassistant/components/tuya/translations/sensor.pt.json create mode 100644 homeassistant/components/watttime/translations/pt.json create mode 100644 homeassistant/components/withings/translations/af.json create mode 100644 homeassistant/components/withings/translations/ar.json create mode 100644 homeassistant/components/withings/translations/bn.json create mode 100644 homeassistant/components/withings/translations/bs.json create mode 100644 homeassistant/components/withings/translations/eu.json create mode 100644 homeassistant/components/withings/translations/hy.json create mode 100644 homeassistant/components/withings/translations/zh-Hans.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.pt.json diff --git a/homeassistant/components/ambee/translations/pt.json b/homeassistant/components/ambee/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/ambee/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index 9d7fdda1006..d204347bd02 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -89,6 +89,9 @@ "vibration": "foram detectadas vibra\u00e7\u00f5es em {entity_name}" } }, + "device_class": { + "moisture": "humidade" + }, "state": { "_": { "off": "Desligado", @@ -178,6 +181,9 @@ "off": "Limpo", "on": "Detectado" }, + "update": { + "off": "Actualizado" + }, "vibration": { "off": "Limpo", "on": "Detetado" diff --git a/homeassistant/components/climacell/translations/sensor.pt.json b/homeassistant/components/climacell/translations/sensor.pt.json new file mode 100644 index 00000000000..30ba0f75808 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy_for_sensitive_groups": "Pouco saud\u00e1vel para grupos sens\u00edveis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index 94efb5c68ca..53dfd5b29bf 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -54,6 +54,7 @@ "remote_button_double_press": "Bot\u00e3o \"{subtype}\" clicado duas vezes", "remote_button_long_press": "Bot\u00e3o \"{subtype}\" pressionado continuamente", "remote_falling": "Dispositivo em queda livre", + "remote_flip_180_degrees": "Dispositivo virado 180 graus", "remote_gyro_activated": "Dispositivo agitado" } }, diff --git a/homeassistant/components/demo/translations/pt.json b/homeassistant/components/demo/translations/pt.json index db34017d6e2..7d9ee992b39 100644 --- a/homeassistant/components/demo/translations/pt.json +++ b/homeassistant/components/demo/translations/pt.json @@ -3,6 +3,7 @@ "step": { "options_1": { "data": { + "bool": "Booleano opcional", "constant": "Constante" } } diff --git a/homeassistant/components/derivative/translations/pt.json b/homeassistant/components/derivative/translations/pt.json new file mode 100644 index 00000000000..d6c0f4acd0b --- /dev/null +++ b/homeassistant/components/derivative/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "round": "Precis\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/pt.json b/homeassistant/components/econet/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/econet/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/pt.json b/homeassistant/components/evil_genius_labs/translations/pt.json new file mode 100644 index 00000000000..4e8578a0a28 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Anfitri\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/pt.json b/homeassistant/components/ezviz/translations/pt.json new file mode 100644 index 00000000000..cd669c3fd29 --- /dev/null +++ b/homeassistant/components/ezviz/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/pt.json b/homeassistant/components/flipr/translations/pt.json new file mode 100644 index 00000000000..ce1bf4bb4b8 --- /dev/null +++ b/homeassistant/components/flipr/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pt.json b/homeassistant/components/foscam/translations/pt.json index b8a454fbaba..65a6a1558db 100644 --- a/homeassistant/components/foscam/translations/pt.json +++ b/homeassistant/components/foscam/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/pt.json b/homeassistant/components/fritz/translations/pt.json new file mode 100644 index 00000000000..9eeb4c35b69 --- /dev/null +++ b/homeassistant/components/fritz/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/pt.json b/homeassistant/components/goodwe/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/goodwe/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 940aa088ced..728b1dcbd95 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -9,6 +9,15 @@ } } }, + "options": { + "step": { + "fan": { + "data": { + "hide_members": "Esconder membros" + } + } + } + }, "state": { "_": { "closed": "Fechada", diff --git a/homeassistant/components/harmony/translations/pt.json b/homeassistant/components/harmony/translations/pt.json index 04374af8e82..3b58778917d 100644 --- a/homeassistant/components/harmony/translations/pt.json +++ b/homeassistant/components/harmony/translations/pt.json @@ -14,5 +14,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "A actividade por defeito a executar quando nenhuma \u00e9 especificada." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/pl.json b/homeassistant/components/here_travel_time/translations/pl.json index 3e4f41212a2..17b91417007 100644 --- a/homeassistant/components/here_travel_time/translations/pl.json +++ b/homeassistant/components/here_travel_time/translations/pl.json @@ -39,6 +39,13 @@ }, "title": "Wybierz punkt pocz\u0105tkowy" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Lokalizacja na mapie", + "origin_entity": "Encja" + }, + "title": "Wybierz punkt pocz\u0105tkowy" + }, "user": { "data": { "api_key": "Klucz API", diff --git a/homeassistant/components/here_travel_time/translations/pt.json b/homeassistant/components/here_travel_time/translations/pt.json index f412c0033f6..a36091fc30c 100644 --- a/homeassistant/components/here_travel_time/translations/pt.json +++ b/homeassistant/components/here_travel_time/translations/pt.json @@ -9,5 +9,14 @@ "title": "Escolha a Origem" } } + }, + "options": { + "step": { + "departure_time": { + "data": { + "departure_time": "Hora de partida" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index 09d839cbd5c..8f51f8d74e9 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -32,6 +32,12 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primeiro bot\u00e3o", + "button_4": "Quarto bot\u00e3o" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json index 1a281774ffe..1475c2d00d3 100644 --- a/homeassistant/components/insteon/translations/pt.json +++ b/homeassistant/components/insteon/translations/pt.json @@ -60,8 +60,14 @@ }, "init": { "data": { + "add_x10": "Adicionar um dispositivo X10.", "remove_x10": "Remova um dispositivo X10." } + }, + "remove_override": { + "data": { + "address": "Seleccione um endere\u00e7o de dispositivo para remover" + } } } } diff --git a/homeassistant/components/integration/translations/pt.json b/homeassistant/components/integration/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/integration/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/pt.json b/homeassistant/components/iotawatt/translations/pt.json new file mode 100644 index 00000000000..85b7c4b704a --- /dev/null +++ b/homeassistant/components/iotawatt/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "username": "Nome de utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json index 36962100519..9f8734b5a6d 100644 --- a/homeassistant/components/isy994/translations/pt.json +++ b/homeassistant/components/isy994/translations/pt.json @@ -9,6 +9,11 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/knx/translations/pt.json b/homeassistant/components/knx/translations/pt.json new file mode 100644 index 00000000000..ba21368f797 --- /dev/null +++ b/homeassistant/components/knx/translations/pt.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "tunnel": { + "data": { + "host": "Anfitri\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/pt.json b/homeassistant/components/meteoclimatic/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt.json b/homeassistant/components/nextdns/translations/pt.json new file mode 100644 index 00000000000..92f429b6a45 --- /dev/null +++ b/homeassistant/components/nextdns/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pt.json b/homeassistant/components/nina/translations/pt.json new file mode 100644 index 00000000000..052bde4e432 --- /dev/null +++ b/homeassistant/components/nina/translations/pt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Selecione a cidade/distrito" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/pt.json b/homeassistant/components/nmap_tracker/translations/pt.json new file mode 100644 index 00000000000..8215a0f7a01 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/pt.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "Intervalo de varrimento" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/pt.json b/homeassistant/components/nuheat/translations/pt.json index 7953cf5625c..16a664b4225 100644 --- a/homeassistant/components/nuheat/translations/pt.json +++ b/homeassistant/components/nuheat/translations/pt.json @@ -12,6 +12,7 @@ "user": { "data": { "password": "Palavra-passe", + "serial_number": "N\u00famero de s\u00e9rie do termostato.", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/overkiz/translations/sensor.pt.json b/homeassistant/components/overkiz/translations/sensor.pt.json new file mode 100644 index 00000000000..fd6f8f53478 --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "overkiz__discrete_rssi_level": { + "normal": "Normal" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/pt.json b/homeassistant/components/peco/translations/pt.json new file mode 100644 index 00000000000..9be1378d3e6 --- /dev/null +++ b/homeassistant/components/peco/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "county": "Distrito" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json index e9890abba2f..2eeb965754a 100644 --- a/homeassistant/components/plaato/translations/pt.json +++ b/homeassistant/components/plaato/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "A Conta j\u00e1 est\u00e1 configurada", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/rachio/translations/pt.json b/homeassistant/components/rachio/translations/pt.json index 8a4fdd22177..d6320473ac0 100644 --- a/homeassistant/components/rachio/translations/pt.json +++ b/homeassistant/components/rachio/translations/pt.json @@ -15,5 +15,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Dura\u00e7\u00e3o em minutos para funcionar ao activar um interruptor de zona" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/pt.json b/homeassistant/components/rainforest_eagle/translations/pt.json new file mode 100644 index 00000000000..4e8578a0a28 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Anfitri\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/et.json b/homeassistant/components/rhasspy/translations/et.json new file mode 100644 index 00000000000..8182211cdef --- /dev/null +++ b/homeassistant/components/rhasspy/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine." + }, + "step": { + "user": { + "description": "Kas soovid Rhaspy toe lubada?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/fr.json b/homeassistant/components/rhasspy/translations/fr.json new file mode 100644 index 00000000000..6fce2e23902 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous activer la prise en charge de Rhasspy\u00a0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/pt-BR.json b/homeassistant/components/rhasspy/translations/pt-BR.json new file mode 100644 index 00000000000..0e62dceb57d --- /dev/null +++ b/homeassistant/components/rhasspy/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 foi configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja habilitar o suporte ao Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json index 84fe0402f15..3afb1cc22e3 100644 --- a/homeassistant/components/roomba/translations/pt.json +++ b/homeassistant/components/roomba/translations/pt.json @@ -11,6 +11,11 @@ "link": { "title": "Recuperar Palavra-passe" }, + "manual": { + "data": { + "host": "Anfitri\u00e3o" + } + }, "user": { "data": { "host": "Servidor" diff --git a/homeassistant/components/select/translations/pt.json b/homeassistant/components/select/translations/pt.json new file mode 100644 index 00000000000..aa92ddaf4c3 --- /dev/null +++ b/homeassistant/components/select/translations/pt.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Alterar op\u00e7\u00e3o {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pt.json b/homeassistant/components/shelly/translations/pt.json index d66cc0e5dd9..43f18a5c9d6 100644 --- a/homeassistant/components/shelly/translations/pt.json +++ b/homeassistant/components/shelly/translations/pt.json @@ -25,5 +25,10 @@ } } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Bot\u00e3o" + } } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/pt.json b/homeassistant/components/squeezebox/translations/pt.json index 97ced548ed5..86aa270e47d 100644 --- a/homeassistant/components/squeezebox/translations/pt.json +++ b/homeassistant/components/squeezebox/translations/pt.json @@ -17,7 +17,8 @@ "password": "Palavra-passe", "port": "Porta", "username": "Utilizador" - } + }, + "title": "Editar informa\u00e7\u00e3o sobre a liga\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/tankerkoenig/translations/pt.json b/homeassistant/components/tankerkoenig/translations/pt.json new file mode 100644 index 00000000000..19646bbce9c --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome da regi\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.pt.json b/homeassistant/components/tomorrowio/translations/sensor.pt.json new file mode 100644 index 00000000000..db42eacabaa --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "tomorrowio__health_concern": { + "moderate": "Moderado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.pt.json b/homeassistant/components/tuya/translations/select.pt.json new file mode 100644 index 00000000000..77604504f4d --- /dev/null +++ b/homeassistant/components/tuya/translations/select.pt.json @@ -0,0 +1,14 @@ +{ + "state": { + "tuya__record_mode": { + "2": "Grava\u00e7\u00e3o cont\u00ednua" + }, + "tuya__relay_status": { + "on": "Ligado" + }, + "tuya__vacuum_mode": { + "pick_zone": "Escolha a Zona", + "right_spiral": "Espiral Direita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.pt.json b/homeassistant/components/tuya/translations/sensor.pt.json new file mode 100644 index 00000000000..9547600f35f --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "tuya__air_quality": { + "good": "Bom" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/pt.json b/homeassistant/components/upb/translations/pt.json index 657ce03e544..3bd5d8358a6 100644 --- a/homeassistant/components/upb/translations/pt.json +++ b/homeassistant/components/upb/translations/pt.json @@ -6,6 +6,13 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "protocol": "Protocolo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/pt.json b/homeassistant/components/watttime/translations/pt.json new file mode 100644 index 00000000000..d64652ed815 --- /dev/null +++ b/homeassistant/components/watttime/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Nome de utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/af.json b/homeassistant/components/withings/translations/af.json new file mode 100644 index 00000000000..3a1c3f97dbf --- /dev/null +++ b/homeassistant/components/withings/translations/af.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Herverifieer integrasie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ar.json b/homeassistant/components/withings/translations/ar.json new file mode 100644 index 00000000000..8db98ed45e5 --- /dev/null +++ b/homeassistant/components/withings/translations/ar.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u0625\u0639\u0627\u062f\u0629 \u0645\u0635\u0627\u062f\u0642\u0629 \u0627\u0644\u062a\u0643\u0627\u0645\u0644" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/bg.json b/homeassistant/components/withings/translations/bg.json index 9d5313ee391..ad1284c7a48 100644 --- a/homeassistant/components/withings/translations/bg.json +++ b/homeassistant/components/withings/translations/bg.json @@ -20,6 +20,9 @@ }, "reauth": { "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } } diff --git a/homeassistant/components/withings/translations/bn.json b/homeassistant/components/withings/translations/bn.json new file mode 100644 index 00000000000..97f6140ff26 --- /dev/null +++ b/homeassistant/components/withings/translations/bn.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u0987\u09a8\u09cd\u099f\u09bf\u0997\u09cd\u09b0\u09c7\u09b6\u09a8 \u09aa\u09c1\u09a8\u09b0\u09be\u09af\u09bc \u09aa\u09cd\u09b0\u09ae\u09be\u09a3\u09c0\u0995\u09b0\u09a3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/bs.json b/homeassistant/components/withings/translations/bs.json new file mode 100644 index 00000000000..14583e8ae80 --- /dev/null +++ b/homeassistant/components/withings/translations/bs.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Ponovo potvrdite integraciju" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index c75e08e0234..c7ec9fcd8ca 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -27,6 +27,9 @@ "reauth": { "description": "El perfil \"{profile}\" s'ha de tornar a autenticar per poder continuar rebent dades de Withings.", "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "reauth_confirm": { + "title": "Reautenticar la integraci\u00f3" } } } diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index 31d5ad2f6e5..672ced9ca5c 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -27,6 +27,10 @@ "reauth": { "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.", "title": "Integration erneut authentifizieren" + }, + "reauth_confirm": { + "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.", + "title": "Integration erneut authentifizieren" } } } diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json index ca969626510..490e60512f9 100644 --- a/homeassistant/components/withings/translations/en.json +++ b/homeassistant/components/withings/translations/en.json @@ -24,6 +24,10 @@ "description": "Provide a unique profile name for this data. Typically this is the name of the profile you selected in the previous step.", "title": "User Profile." }, + "reauth": { + "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", + "title": "Reauthenticate Integration" + }, "reauth_confirm": { "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", "title": "Reauthenticate Integration" diff --git a/homeassistant/components/withings/translations/et.json b/homeassistant/components/withings/translations/et.json index 5395069522f..a336b6b0bdf 100644 --- a/homeassistant/components/withings/translations/et.json +++ b/homeassistant/components/withings/translations/et.json @@ -27,6 +27,10 @@ "reauth": { "description": "Withingi andmete jsaamiseks tuleb kasutaja {profile} taastuvastada.", "title": "Taastuvasta sidumine" + }, + "reauth_confirm": { + "description": "Profiil \"{profile}\" tuleb uuesti tuvastada, et j\u00e4tkata Withingsi andmete saamist.", + "title": "Taastuvasta sidumine" } } } diff --git a/homeassistant/components/withings/translations/eu.json b/homeassistant/components/withings/translations/eu.json new file mode 100644 index 00000000000..fa2d5af97c2 --- /dev/null +++ b/homeassistant/components/withings/translations/eu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Berriro autentifikatu Integrazioa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index f59b8e2e714..e0d9e8db08d 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -25,7 +25,11 @@ "title": "Profil utilisateur" }, "reauth": { - "description": "Le profile \" {profile} \" doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", + "description": "Le profile \u00ab\u00a0{profile}\u00a0\u00bb doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "reauth_confirm": { + "description": "Le profile \u00ab\u00a0{profile}\u00a0\u00bb doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", "title": "R\u00e9-authentifier l'int\u00e9gration" } } diff --git a/homeassistant/components/withings/translations/hy.json b/homeassistant/components/withings/translations/hy.json new file mode 100644 index 00000000000..92c4ce24080 --- /dev/null +++ b/homeassistant/components/withings/translations/hy.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u054e\u0565\u0580\u0561\u0570\u0561\u057d\u057f\u0561\u057f\u0565\u056c \u056b\u0576\u057f\u0565\u0563\u0580\u0578\u0582\u0574\u0568" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/pl.json b/homeassistant/components/withings/translations/pl.json index 638171af846..27544b29fa4 100644 --- a/homeassistant/components/withings/translations/pl.json +++ b/homeassistant/components/withings/translations/pl.json @@ -27,6 +27,10 @@ "reauth": { "description": "Profil \"{profile}\" musi zosta\u0107 ponownie uwierzytelniony, aby nadal otrzymywa\u0107 dane Withings.", "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "reauth_confirm": { + "description": "Profil \"{profile}\" musi zosta\u0107 ponownie uwierzytelniony, aby nadal otrzymywa\u0107 dane Withings.", + "title": "Ponownie uwierzytelnij integracj\u0119" } } } diff --git a/homeassistant/components/withings/translations/pt-BR.json b/homeassistant/components/withings/translations/pt-BR.json index 6a067498f1e..4ea2fd4a92c 100644 --- a/homeassistant/components/withings/translations/pt-BR.json +++ b/homeassistant/components/withings/translations/pt-BR.json @@ -27,6 +27,10 @@ "reauth": { "description": "O perfil \"{profile}\" precisa ser autenticado novamente para continuar recebendo dados do Withings", "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "O perfil \"{profile}\" precisa ser autenticado novamente para continuar recebendo dados do Withings.", + "title": "Reautenticar Integra\u00e7\u00e3o" } } } diff --git a/homeassistant/components/withings/translations/zh-Hans.json b/homeassistant/components/withings/translations/zh-Hans.json new file mode 100644 index 00000000000..83a6258ba49 --- /dev/null +++ b/homeassistant/components/withings/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u91cd\u65b0\u9a8c\u8bc1\u96c6\u6210" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json index 2ee7ce3d3da..35328ea9353 100644 --- a/homeassistant/components/withings/translations/zh-Hant.json +++ b/homeassistant/components/withings/translations/zh-Hant.json @@ -27,6 +27,10 @@ "reauth": { "description": "\"{profile}\" \u8a2d\u5b9a\u6a94\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u4ee5\u4fdd\u6301\u63a5\u6536 Withings \u8cc7\u6599\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "reauth_confirm": { + "description": "\"{profile}\" \u8a2d\u5b9a\u6a94\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u4ee5\u4fdd\u6301\u63a5\u6536 Withings \u8cc7\u6599\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } } diff --git a/homeassistant/components/wled/translations/pt.json b/homeassistant/components/wled/translations/pt.json index 313c9057da0..cc9f0af829a 100644 --- a/homeassistant/components/wled/translations/pt.json +++ b/homeassistant/components/wled/translations/pt.json @@ -12,6 +12,9 @@ "data": { "host": "Nome servidor ou endere\u00e7o IP" } + }, + "zeroconf_confirm": { + "title": "Dispositivo WLED descoberto" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.pt.json b/homeassistant/components/xiaomi_miio/translations/select.pt.json new file mode 100644 index 00000000000..24ed8a3e752 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "dim": "Escurecer" + } + } +} \ No newline at end of file From f4953e6e9b0708f6f05aa8723c101966d16cfb84 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 12 Jul 2022 01:42:33 -0500 Subject: [PATCH 392/820] Do not spam log when Life360 member location is missing (#75029) --- homeassistant/components/life360/__init__.py | 6 +- .../components/life360/coordinator.py | 62 +++++++++++++++---- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index 4527f6ac298..d05a9ec400b 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -37,7 +37,7 @@ from .const import ( SHOW_DRIVING, SHOW_MOVING, ) -from .coordinator import Life360DataUpdateCoordinator +from .coordinator import Life360DataUpdateCoordinator, MissingLocReason PLATFORMS = [Platform.DEVICE_TRACKER] @@ -128,6 +128,10 @@ class IntegData: coordinators: dict[str, Life360DataUpdateCoordinator] = field( init=False, default_factory=dict ) + # member_id: missing location reason + missing_loc_reason: dict[str, MissingLocReason] = field( + init=False, default_factory=dict + ) # member_id: ConfigEntry.entry_id tracked_members: dict[str, str] = field(init=False, default_factory=dict) logged_circles: list[str] = field(init=False, default_factory=list) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index dc7fdb73a8c..05eecd43cdc 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -2,8 +2,10 @@ from __future__ import annotations +from contextlib import suppress from dataclasses import dataclass, field from datetime import datetime +from enum import Enum from typing import Any from life360 import Life360, Life360Error, LoginError @@ -33,6 +35,13 @@ from .const import ( ) +class MissingLocReason(Enum): + """Reason member location information is missing.""" + + VAGUE_ERROR_REASON = "vague error reason" + EXPLICIT_ERROR_REASON = "explicit error reason" + + @dataclass class Life360Place: """Life360 Place data.""" @@ -99,6 +108,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): max_retries=COMM_MAX_RETRIES, authorization=entry.data[CONF_AUTHORIZATION], ) + self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason async def _retrieve_data(self, func: str, *args: Any) -> list[dict[str, Any]]: """Get data from Life360.""" @@ -141,10 +151,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): if not int(member["features"]["shareLocation"]): continue - # Note that member may be in more than one circle. If that's the case just - # go ahead and process the newly retrieved data (overwriting the older - # data), since it might be slightly newer than what was retrieved while - # processing another circle. + member_id = member["id"] first = member["firstName"] last = member["lastName"] @@ -153,16 +160,45 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): else: name = first or last - loc = member["location"] - if not loc: - if err_msg := member["issues"]["title"]: - if member["issues"]["dialog"]: - err_msg += f": {member['issues']['dialog']}" - else: - err_msg = "Location information missing" - LOGGER.error("%s: %s", name, err_msg) + cur_missing_reason = self._missing_loc_reason.get(member_id) + + # Check if location information is missing. This can happen if server + # has not heard from member's device in a long time (e.g., has been off + # for a long time, or has lost service, etc.) + if loc := member["location"]: + with suppress(KeyError): + del self._missing_loc_reason[member_id] + else: + if explicit_reason := member["issues"]["title"]: + if extended_reason := member["issues"]["dialog"]: + explicit_reason += f": {extended_reason}" + # Note that different Circles can report missing location in + # different ways. E.g., one might report an explicit reason and + # another does not. If a vague reason has already been logged but a + # more explicit reason is now available, log that, too. + if ( + cur_missing_reason is None + or cur_missing_reason == MissingLocReason.VAGUE_ERROR_REASON + and explicit_reason + ): + if explicit_reason: + self._missing_loc_reason[ + member_id + ] = MissingLocReason.EXPLICIT_ERROR_REASON + err_msg = explicit_reason + else: + self._missing_loc_reason[ + member_id + ] = MissingLocReason.VAGUE_ERROR_REASON + err_msg = "Location information missing" + LOGGER.error("%s: %s", name, err_msg) continue + # Note that member may be in more than one circle. If that's the case + # just go ahead and process the newly retrieved data (overwriting the + # older data), since it might be slightly newer than what was retrieved + # while processing another circle. + place = loc["name"] or None if place: @@ -179,7 +215,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): if self._hass.config.units.is_metric: speed = convert(speed, LENGTH_MILES, LENGTH_KILOMETERS) - data.members[member["id"]] = Life360Member( + data.members[member_id] = Life360Member( address, dt_util.utc_from_timestamp(int(loc["since"])), bool(int(loc["charge"])), From dfe840c04599d475e31f3d9a3c76fb61d4bedbd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Jul 2022 08:51:24 +0200 Subject: [PATCH 393/820] Bump actions/setup-python from 4.0.0 to 4.1.0 (#75040) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 24 ++++++++++++------------ .github/workflows/translations.yaml | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index d49f1e3a9c7..ac30becb128 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -115,7 +115,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5320b9fe474..a5f3da97ac4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -166,7 +166,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} cache: "pip" @@ -205,7 +205,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -254,7 +254,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -306,7 +306,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -347,7 +347,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -470,7 +470,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ matrix.python-version }} - name: Generate partial pip restore key @@ -533,7 +533,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment @@ -565,7 +565,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -598,7 +598,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment @@ -642,7 +642,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment @@ -690,7 +690,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment @@ -744,7 +744,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 0d3ddc4ca18..bc9fa63c86c 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} From 6a376009364935af7524751c8bff10097a0ce3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Tue, 12 Jul 2022 10:01:53 +0200 Subject: [PATCH 394/820] Remove incorrect device class from blebox button (#75042) * Removed redundant attr device class from button. * Removed irrelevant test for checking buttons device_class. --- homeassistant/components/blebox/button.py | 3 +-- tests/components/blebox/test_button.py | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py index 01de5fa56b7..e9ceaac2dc7 100644 --- a/homeassistant/components/blebox/button.py +++ b/homeassistant/components/blebox/button.py @@ -1,7 +1,7 @@ """BleBox button entities implementation.""" from __future__ import annotations -from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,7 +26,6 @@ class BleBoxButtonEntity(BleBoxEntity, ButtonEntity): def __init__(self, feature): """Initialize a BleBox button feature.""" super().__init__(feature) - self._attr_device_class = ButtonDeviceClass.UPDATE self._attr_icon = self.get_icon() def get_icon(self): diff --git a/tests/components/blebox/test_button.py b/tests/components/blebox/test_button.py index 22e1b8cb734..8d89f31d260 100644 --- a/tests/components/blebox/test_button.py +++ b/tests/components/blebox/test_button.py @@ -5,7 +5,6 @@ from unittest.mock import PropertyMock import blebox_uniapi import pytest -from homeassistant.components.button import ButtonDeviceClass from homeassistant.const import ATTR_ICON from .conftest import async_setup_entity, mock_feature @@ -50,8 +49,6 @@ async def test_tvliftbox_init(tvliftbox, hass, config, caplog): assert entry.unique_id == "BleBox-tvLiftBox-4a3fdaad90aa-open_or_stop" - assert state.attributes["device_class"] == ButtonDeviceClass.UPDATE - assert state.name == "tvLiftBox-open_or_stop" From 5930f056a8245d8f27a7d54cb2c126a64eb13d98 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 12 Jul 2022 11:07:18 +0200 Subject: [PATCH 395/820] Mqtt support config_entry unload (#70149) * squashed commits for rebase * Flake * Fix reloading issue manual legacy items * Improve ACS sync for unsubscribe at disconnect * Processed review comments * Update homeassistant/components/mqtt/client.py Co-authored-by: Erik Montnemery * No need to await entry setup * Remove complication is_connected * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 126 +++++--- .../components/mqtt/alarm_control_panel.py | 8 +- .../components/mqtt/binary_sensor.py | 8 +- homeassistant/components/mqtt/button.py | 8 +- homeassistant/components/mqtt/camera.py | 8 +- homeassistant/components/mqtt/client.py | 59 +++- homeassistant/components/mqtt/climate.py | 8 +- homeassistant/components/mqtt/const.py | 4 +- homeassistant/components/mqtt/cover.py | 8 +- .../mqtt/device_tracker/__init__.py | 21 +- .../mqtt/device_tracker/schema_discovery.py | 23 +- .../mqtt/device_tracker/schema_yaml.py | 44 ++- homeassistant/components/mqtt/discovery.py | 3 +- homeassistant/components/mqtt/fan.py | 8 +- homeassistant/components/mqtt/humidifier.py | 8 +- .../components/mqtt/light/__init__.py | 11 +- homeassistant/components/mqtt/lock.py | 8 +- homeassistant/components/mqtt/mixins.py | 56 +++- homeassistant/components/mqtt/number.py | 8 +- homeassistant/components/mqtt/scene.py | 8 +- homeassistant/components/mqtt/select.py | 8 +- homeassistant/components/mqtt/sensor.py | 8 +- homeassistant/components/mqtt/siren.py | 8 +- homeassistant/components/mqtt/switch.py | 8 +- homeassistant/components/mqtt/util.py | 12 + .../components/mqtt/vacuum/__init__.py | 13 +- .../mqtt/test_alarm_control_panel.py | 10 + tests/components/mqtt/test_binary_sensor.py | 10 + tests/components/mqtt/test_button.py | 10 + tests/components/mqtt/test_camera.py | 10 + tests/components/mqtt/test_climate.py | 10 + tests/components/mqtt/test_common.py | 95 ++++++- tests/components/mqtt/test_config_flow.py | 7 +- tests/components/mqtt/test_cover.py | 10 + tests/components/mqtt/test_device_tracker.py | 120 +++++++- tests/components/mqtt/test_device_trigger.py | 52 ++++ tests/components/mqtt/test_fan.py | 10 + tests/components/mqtt/test_humidifier.py | 10 + tests/components/mqtt/test_init.py | 268 +++++++++++++++++- tests/components/mqtt/test_light.py | 10 + tests/components/mqtt/test_light_template.py | 10 + tests/components/mqtt/test_lock.py | 10 + tests/components/mqtt/test_number.py | 10 + tests/components/mqtt/test_scene.py | 10 + tests/components/mqtt/test_select.py | 10 + tests/components/mqtt/test_sensor.py | 10 + tests/components/mqtt/test_siren.py | 10 + tests/components/mqtt/test_switch.py | 10 + tests/components/mqtt/test_tag.py | 27 ++ 49 files changed, 1107 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 143d97b928f..906923138b9 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -11,7 +11,7 @@ from typing import Any, cast import jinja2 import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config as conf_util, config_entries from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -20,10 +20,9 @@ from homeassistant.const import ( CONF_PAYLOAD, CONF_PORT, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) -from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback +from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template @@ -65,9 +64,10 @@ from .const import ( # noqa: F401 CONF_TOPIC, CONF_WILL_MESSAGE, CONFIG_ENTRY_IS_SETUP, - DATA_CONFIG_ENTRY_LOCK, DATA_MQTT, DATA_MQTT_CONFIG, + DATA_MQTT_RELOAD_DISPATCHERS, + DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_RELOAD_NEEDED, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, @@ -87,7 +87,12 @@ from .models import ( # noqa: F401 ReceiveMessage, ReceivePayloadType, ) -from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic +from .util import ( + _VALID_QOS_SCHEMA, + mqtt_config_entry_enabled, + valid_publish_topic, + valid_subscribe_topic, +) _LOGGER = logging.getLogger(__name__) @@ -174,7 +179,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf = dict(conf) hass.data[DATA_MQTT_CONFIG] = conf - if not bool(hass.config_entries.async_entries(DOMAIN)): + if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is None: # Create an import flow if the user has yaml configured entities etc. # but no broker configuration. Note: The intention is not for this to # import broker configuration from YAML because that has been deprecated. @@ -185,6 +190,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: data={}, ) ) + hass.data[DATA_MQTT_RELOAD_NEEDED] = True + elif mqtt_entry_status is False: + _LOGGER.info( + "MQTT will be not available until the config entry is enabled", + ) + hass.data[DATA_MQTT_RELOAD_NEEDED] = True + return True @@ -239,17 +251,18 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: - """Load a config entry.""" - # Merge basic configuration, and add missing defaults for basic options - _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) +async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None: + """Fetch fresh MQTT yaml config from the hass config when (re)loading the entry.""" + if DATA_MQTT_RELOAD_ENTRY in hass.data: + hass_config = await conf_util.async_hass_config_yaml(hass) + mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) + hass.data[DATA_MQTT_CONFIG] = mqtt_config + _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) # Bail out if broker setting is missing if CONF_BROKER not in entry.data: _LOGGER.error("MQTT broker is not configured, please configure it") - return False + return None # If user doesn't have configuration.yaml config, generate default values # for options not in config entry data @@ -271,22 +284,21 @@ async def async_setup_entry( # noqa: C901 # Merge advanced configuration values from configuration.yaml conf = _merge_extended_config(entry, conf) + return conf - hass.data[DATA_MQTT] = MQTT( - hass, - entry, - conf, - ) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Load a config entry.""" + # Merge basic configuration, and add missing defaults for basic options + if (conf := await async_fetch_config(hass, entry)) is None: + # Bail out + return False + + hass.data[DATA_MQTT] = MQTT(hass, entry, conf) entry.add_update_listener(_async_config_entry_updated) await hass.data[DATA_MQTT].async_connect() - async def async_stop_mqtt(_event: Event): - """Stop MQTT component.""" - await hass.data[DATA_MQTT].async_disconnect() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) - async def async_publish_service(call: ServiceCall) -> None: """Handle MQTT publish service calls.""" msg_topic = call.data.get(ATTR_TOPIC) @@ -375,7 +387,6 @@ async def async_setup_entry( # noqa: C901 ) # setup platforms and discovery - hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() async def async_setup_reload_service() -> None: @@ -411,6 +422,7 @@ async def async_setup_entry( # noqa: C901 # pylint: disable-next=import-outside-toplevel from . import device_automation, tag + # Forward the entry setup to the MQTT platforms await asyncio.gather( *( [ @@ -428,21 +440,25 @@ async def async_setup_entry( # noqa: C901 await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded await async_setup_reload_service() - if DATA_MQTT_RELOAD_NEEDED in hass.data: hass.data.pop(DATA_MQTT_RELOAD_NEEDED) - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=False, - ) + await async_reload_manual_mqtt_items(hass) await async_forward_entry_setup_and_setup_discovery(entry) return True +async def async_reload_manual_mqtt_items(hass: HomeAssistant) -> None: + """Reload manual configured MQTT items.""" + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + + @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} ) @@ -544,3 +560,49 @@ async def async_remove_config_entry_device( await device_automation.async_removed_from_device(hass, device_entry.id) return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload MQTT dump and publish service when the config entry is unloaded.""" + # Unload publish and dump services. + hass.services.async_remove( + DOMAIN, + SERVICE_PUBLISH, + ) + hass.services.async_remove( + DOMAIN, + SERVICE_DUMP, + ) + + # Stop the discovery + await discovery.async_stop(hass) + mqtt_client: MQTT = hass.data[DATA_MQTT] + # Unload the platforms + await asyncio.gather( + *( + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ) + ) + await hass.async_block_till_done() + # Unsubscribe reload dispatchers + while reload_dispatchers := hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []): + reload_dispatchers.pop()() + hass.data[CONFIG_ENTRY_IS_SETUP] = set() + # Cleanup listeners + mqtt_client.cleanup() + + # Trigger reload manual MQTT items at entry setup + # Reload the legacy yaml platform + await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False: + # The entry is disabled reload legacy manual items when the entry is enabled again + hass.data[DATA_MQTT_RELOAD_NEEDED] = True + elif mqtt_entry_status is True: + # The entry is reloaded: + # Trigger re-fetching the yaml config at entry setup + hass.data[DATA_MQTT_RELOAD_ENTRY] = True + # Stop the loop + await mqtt_client.async_disconnect() + + return True diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index b6f2f8f236e..cf7262f9468 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -156,8 +156,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Alarm Control Panel platform.""" async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 012c6e81ac8..cffb2fd8300 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -112,8 +112,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT binary sensor.""" async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index b75fbe4b97f..0881b963b04 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -91,8 +91,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT button.""" async_add_entities([MqttButton(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 69af7992229..f213bec9bb6 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -89,8 +89,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Camera.""" async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index d676c128260..9eeed426d17 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -23,8 +23,9 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, HassJob, HomeAssistant, callback +from homeassistant.core import CoreState, Event, HassJob, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -59,6 +60,7 @@ from .models import ( ReceiveMessage, ReceivePayloadType, ) +from .util import mqtt_config_entry_enabled if TYPE_CHECKING: # Only import for paho-mqtt type checking here, imports are done locally @@ -95,6 +97,10 @@ async def async_publish( ) -> None: """Publish message to a MQTT topic.""" + if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + raise HomeAssistantError( + f"Cannot publish to topic '{topic}', MQTT is not enabled" + ) outgoing_payload = payload if not isinstance(payload, bytes): if not encoding: @@ -174,6 +180,10 @@ async def async_subscribe( Call the return value to unsubscribe. """ + if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + raise HomeAssistantError( + f"Cannot subscribe to topic '{topic}', MQTT is not enabled" + ) # Count callback parameters which don't have a default value non_default = 0 if msg_callback: @@ -316,6 +326,8 @@ class MQTT: self._last_subscribe = time.time() self._mqttc: mqtt.Client = None self._paho_lock = asyncio.Lock() + self._pending_acks: set[int] = set() + self._cleanup_on_unload: list[Callable] = [] self._pending_operations: dict[str, asyncio.Event] = {} @@ -331,6 +343,20 @@ class MQTT: self.init_client() + @callback + async def async_stop_mqtt(_event: Event): + """Stop MQTT component.""" + await self.async_disconnect() + + self._cleanup_on_unload.append( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) + ) + + def cleanup(self): + """Clean up listeners.""" + while self._cleanup_on_unload: + self._cleanup_on_unload.pop()() + def init_client(self): """Initialize paho client.""" self._mqttc = MqttClientSetup(self.conf).client @@ -405,6 +431,15 @@ class MQTT: # Do not disconnect, we want the broker to always publish will self._mqttc.loop_stop() + # wait for ACK-s to be processes (unsubscribe only) + async with self._paho_lock: + tasks = [ + self.hass.async_create_task(self._wait_for_mid(mid)) + for mid in self._pending_acks + ] + await asyncio.gather(*tasks) + + # stop the MQTT loop await self.hass.async_add_executor_job(stop) async def async_subscribe( @@ -440,7 +475,7 @@ class MQTT: self.subscriptions.remove(subscription) self._matching_subscriptions.cache_clear() - # Only unsubscribe if currently connected. + # Only unsubscribe if currently connected if self.connected: self.hass.async_create_task(self._async_unsubscribe(topic)) @@ -451,18 +486,20 @@ class MQTT: This method is a coroutine. """ + + def _client_unsubscribe(topic: str) -> None: + result: int | None = None + result, mid = self._mqttc.unsubscribe(topic) + _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) + _raise_on_error(result) + self._pending_acks.add(mid) + if any(other.topic == topic for other in self.subscriptions): # Other subscriptions on topic remaining - don't unsubscribe. return async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.unsubscribe, topic - ) - _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) + await self.hass.async_add_executor_job(_client_unsubscribe, topic) async def _async_perform_subscriptions( self, subscriptions: Iterable[tuple[str, int]] @@ -643,6 +680,10 @@ class MQTT: ) finally: del self._pending_operations[mid] + # Cleanup ACK sync buffer + async with self._paho_lock: + if mid in self._pending_acks: + self._pending_acks.remove(mid) async def _discovery_cooldown(self): now = time.time() diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 80ff33309ba..30263798740 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -401,8 +401,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT climate devices.""" async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6ac77021337..6a5cb912fce 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -31,9 +31,11 @@ CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" -DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_MQTT = "mqtt" DATA_MQTT_CONFIG = "mqtt_config" +MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" +DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers" +DATA_MQTT_RELOAD_ENTRY = "mqtt_reload_entry" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 14746329250..b0fbacd10fc 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -251,8 +251,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Cover.""" async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index 1b6c2b25ff3..99e0c87044b 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -2,7 +2,11 @@ import voluptuous as vol from homeassistant.components import device_tracker +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from ..const import MQTT_DATA_DEVICE_TRACKER_LEGACY from ..mixins import warn_for_legacy_schema from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401 from .schema_discovery import async_setup_entry_from_discovery @@ -12,5 +16,20 @@ from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA_YAML, warn_for_legacy_schema(device_tracker.DOMAIN) ) + +# Legacy setup async_setup_scanner = async_setup_scanner_from_yaml -async_setup_entry = async_setup_entry_from_discovery + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT device_tracker through configuration.yaml and dynamically through MQTT discovery.""" + await async_setup_entry_from_discovery(hass, config_entry, async_add_entities) + # (re)load legacy service + if MQTT_DATA_DEVICE_TRACKER_LEGACY in hass.data: + await async_setup_scanner_from_yaml( + hass, **hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] + ) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 1ba540c8243..105442b176e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,4 +1,6 @@ -"""Support for tracking MQTT enabled devices.""" +"""Support for tracking MQTT enabled devices identified through discovery.""" +from __future__ import annotations + import asyncio import functools @@ -7,6 +9,7 @@ import voluptuous as vol from homeassistant.components import device_tracker from homeassistant.components.device_tracker import SOURCE_TYPES from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, @@ -16,8 +19,10 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType from .. import subscription from ..config import MQTT_RO_SCHEMA @@ -47,7 +52,11 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): +async def async_setup_entry_from_discovery( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml await asyncio.gather( @@ -66,8 +75,12 @@ async def async_setup_entry_from_discovery(hass, config_entry, async_add_entitie async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Device Tracker entity.""" async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py index 2dfa5b7134c..1990b380dcb 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -1,16 +1,23 @@ """Support for tracking MQTT enabled devices defined in YAML.""" +from collections.abc import Callable +import logging +from typing import Any + import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from ... import mqtt from ..client import async_subscribe from ..config import SCHEMA_BASE -from ..const import CONF_QOS -from ..util import valid_subscribe_topic +from ..const import CONF_QOS, MQTT_DATA_DEVICE_TRACKER_LEGACY +from ..util import mqtt_config_entry_enabled, valid_subscribe_topic + +_LOGGER = logging.getLogger(__name__) CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" @@ -26,13 +33,34 @@ PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend( ) -async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info=None): +async def async_setup_scanner_from_yaml( + hass: HomeAssistant, config, async_see, discovery_info=None +): """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] payload_home = config[CONF_PAYLOAD_HOME] payload_not_home = config[CONF_PAYLOAD_NOT_HOME] source_type = config.get(CONF_SOURCE_TYPE) + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + subscriptions: list[Callable] = [] + + hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] = { + "async_see": async_see, + "config": config, + } + if not mqtt_config_entry_enabled(hass): + _LOGGER.info( + "MQTT device trackers will be not available until the config entry is enabled", + ) + return + + @callback + def _entry_unload(*_: Any) -> None: + """Handle the unload of the config entry.""" + # Unsubscribe from mqtt + for unsubscribe in subscriptions: + unsubscribe() for dev_id, topic in devices.items(): @@ -52,6 +80,10 @@ async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info= hass.async_create_task(async_see(**see_args)) - await async_subscribe(hass, topic, async_message_received, qos) + subscriptions.append( + await async_subscribe(hass, topic, async_message_received, qos) + ) + + config_entry.async_on_unload(_entry_unload) return True diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 5b39e8fa1b5..a480cdd5680 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -234,8 +234,7 @@ async def async_start( # noqa: C901 hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None ) - hass.data[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock() - + hass.data.setdefault(DATA_CONFIG_FLOW_LOCK, asyncio.Lock()) hass.data[ALREADY_DISCOVERED] = {} hass.data[PENDING_DISCOVERED] = {} diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 15e4a80f3e7..20c4936ab38 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -241,8 +241,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT fan.""" async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 43ff2af65d4..3a1271ea2c9 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -197,8 +197,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT humidifier.""" async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index c7f3395ba4e..76c2980e63b 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,6 +1,7 @@ """Support for MQTT lights.""" from __future__ import annotations +from collections.abc import Callable import functools import voluptuous as vol @@ -120,10 +121,14 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up a MQTT Light.""" - setup_entity = { + setup_entity: dict[str, Callable] = { "basic": async_setup_entity_basic, "json": async_setup_entity_json, "template": async_setup_entity_template, diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index b4788f1db0c..4910eafae75 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -112,8 +112,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Lock platform.""" async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8fe9ee564de..af0660b4c93 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -3,7 +3,8 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine +from functools import partial import logging from typing import Any, Protocol, cast, final @@ -61,7 +62,7 @@ from .const import ( CONF_TOPIC, DATA_MQTT, DATA_MQTT_CONFIG, - DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_RELOAD_DISPATCHERS, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, @@ -84,7 +85,7 @@ from .subscription import ( async_subscribe_topics, async_unsubscribe_topics, ) -from .util import valid_subscribe_topic +from .util import mqtt_config_entry_enabled, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -299,11 +300,24 @@ async def async_get_platform_config_from_yaml( return platform_configs -async def async_setup_entry_helper(hass, domain, async_setup, schema): +async def async_setup_entry_helper( + hass: HomeAssistant, + domain: str, + async_setup: partial[Coroutine[HomeAssistant, str, None]], + schema: vol.Schema, +) -> None: """Set up entity, automation or tag creation dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add an MQTT entity, automation or tag.""" + if not mqtt_config_entry_enabled(hass): + _LOGGER.warning( + "MQTT integration is disabled, skipping setup of discovered item " + "MQTT %s, payload %s", + domain, + discovery_payload, + ) + return discovery_data = discovery_payload.discovery_data try: config = schema(discovery_payload) @@ -316,8 +330,10 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): ) raise - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover + hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []).append( + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover + ) ) @@ -328,16 +344,17 @@ async def async_setup_platform_helper( async_add_entities: AddEntitiesCallback, async_setup_entities: SetupEntity, ) -> None: - """Return true if platform setup should be aborted.""" - if not bool(hass.config_entries.async_entries(DOMAIN)): - hass.data[DATA_MQTT_RELOAD_NEEDED] = None + """Help to set up the platform for manual configured MQTT entities.""" + if not (entry_status := mqtt_config_entry_enabled(hass)): _LOGGER.warning( - "MQTT integration is not setup, skipping setup of manually configured " - "MQTT %s", + "MQTT integration is %s, skipping setup of manually configured MQTT %s", + "not setup" if entry_status is None else "disabled", platform_domain, ) return - await async_setup_entities(hass, async_add_entities, config) + # Ensure we set config_entry when entries are set up to enable clean up + config_entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + await async_setup_entities(hass, async_add_entities, config, config_entry) def init_entity_id_from_config(hass, entity, config, entity_id_format): @@ -640,6 +657,7 @@ class MqttDiscoveryDeviceUpdate: MQTT_DISCOVERY_UPDATED.format(discovery_hash), self.async_discovery_update, ) + config_entry.async_on_unload(self._entry_unload) if device_id is not None: self._remove_device_updated = hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self._async_device_removed @@ -650,6 +668,14 @@ class MqttDiscoveryDeviceUpdate: discovery_hash, ) + @callback + def _entry_unload(self, *_: Any) -> None: + """Handle cleanup when the config entry is unloaded.""" + stop_discovery_updates( + self.hass, self._discovery_data, self._remove_discovery_updated + ) + self.hass.async_add_job(self.async_tear_down()) + async def async_discovery_update( self, discovery_payload: DiscoveryInfoType | None, @@ -734,7 +760,11 @@ class MqttDiscoveryDeviceUpdate: class MqttDiscoveryUpdate(Entity): """Mixin used to handle updated discovery message for entity based platforms.""" - def __init__(self, discovery_data, discovery_update=None) -> None: + def __init__( + self, + discovery_data: dict, + discovery_update: Callable | None = None, + ) -> None: """Initialize the discovery update mixin.""" self._discovery_data = discovery_data self._discovery_update = discovery_update diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 9df9dbf818c..fbadd653df7 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -145,8 +145,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT number.""" async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 8b654f7cca0..62de54505eb 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -88,8 +88,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT scene.""" async_add_entities([MqttScene(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index bdf55f895f3..ec88b1732d4 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -103,8 +103,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT select.""" async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 0dda382bec4..4c04d6176f1 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -156,8 +156,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config: ConfigType, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up MQTT sensor.""" async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index f1de0d27de7..5ed76fd6330 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -152,8 +152,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT siren.""" async_add_entities([MqttSiren(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 34009695257..b5c7ab13dfc 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -111,8 +111,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT switch.""" async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 66eec1bdfe8..9ef30da7f3b 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -1,9 +1,13 @@ """Utility functions for the MQTT integration.""" + +from __future__ import annotations + from typing import Any import voluptuous as vol from homeassistant.const import CONF_PAYLOAD +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, template from .const import ( @@ -13,9 +17,17 @@ from .const import ( ATTR_TOPIC, DEFAULT_QOS, DEFAULT_RETAIN, + DOMAIN, ) +def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None: + """Return true when the MQTT config entry is enabled.""" + if not bool(hass.config_entries.async_entries(DOMAIN)): + return None + return not bool(hass.config_entries.async_entries(DOMAIN)[0].disabled_by) + + def valid_topic(value: Any) -> str: """Validate that this is a valid topic name/filter.""" value = cv.string(value) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index c49b8cfa012..cdd14e6d8e3 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -100,10 +100,17 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT vacuum.""" - setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} + setup_entity = { + LEGACY: async_setup_entity_legacy, + STATE: async_setup_entity_state, + } await setup_entity[config[CONF_SCHEMA]]( hass, config, async_add_entities, config_entry, discovery_data ) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index f4a72829046..7d127902f3d 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -60,6 +60,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -971,3 +972,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = alarm_control_panel.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 20037a88d1c..658af79f20f 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -44,6 +44,7 @@ from .test_common import ( help_test_setting_attribute_with_template, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1079,3 +1080,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = binary_sensor.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 8748ef3be4d..68db846c91c 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -37,6 +37,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -482,3 +483,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = button.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 84bf4181a2c..c6d116b6a74 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -36,6 +36,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -346,3 +347,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = camera.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index c633f267e76..aec83a85227 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -55,6 +55,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1881,3 +1882,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = climate.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 92feaa3c109..fe1f4003f0b 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -2,7 +2,7 @@ import copy from datetime import datetime import json -from unittest.mock import ANY, patch +from unittest.mock import ANY, MagicMock, patch import yaml @@ -11,6 +11,7 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.const import MQTT_DISCONNECTED from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -1670,6 +1671,25 @@ async def help_test_reload_with_config(hass, caplog, tmp_path, config): assert "" in caplog.text +async def help_test_entry_reload_with_new_config(hass, tmp_path, new_config): + """Test reloading with supplied config.""" + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump(new_config) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file), patch( + "paho.mqtt.client.Client" + ) as mock_client: + mock_client().connect = lambda *args: 0 + # reload the config entry + assert await hass.config_entries.async_reload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.LOADED + await hass.async_block_till_done() + + async def help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ): @@ -1782,6 +1802,7 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): domain: [new_config_1, new_config_2, new_config_3], } await help_test_reload_with_config(hass, caplog, tmp_path, new_config) + await hass.async_block_till_done() assert len(hass.states.async_all(domain)) == 3 @@ -1792,6 +1813,12 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): async def help_test_setup_manual_entity_from_yaml(hass, platform, config): """Help to test setup from yaml through configuration entry.""" + calls = MagicMock() + + async def mock_reload(hass): + """Mock reload.""" + calls() + config_structure = {mqtt.DOMAIN: {platform: config}} await async_setup_component(hass, mqtt.DOMAIN, config_structure) @@ -1799,7 +1826,71 @@ async def help_test_setup_manual_entity_from_yaml(hass, platform, config): entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_reload_manual_mqtt_items", + side_effect=mock_reload, + ), patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = lambda *args: 0 assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + calls.assert_called_once() + + +async def help_test_unload_config_entry(hass, tmp_path, newconfig): + """Test unloading the MQTT config entry.""" + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump(newconfig) + new_yaml_config_file.write_text(new_yaml_config) + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + await hass.async_block_till_done() + + +async def help_test_unload_config_entry_with_platform( + hass, + mqtt_mock_entry_with_yaml_config, + tmp_path, + domain, + config, +): + """Test unloading the MQTT config entry with a specific platform domain.""" + # prepare setup through configuration.yaml + config_setup = copy.deepcopy(config) + config_setup["name"] = "config_setup" + config_name = config_setup + assert await async_setup_component(hass, domain, {domain: [config_setup]}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + # prepare setup through discovery + discovery_setup = copy.deepcopy(config) + discovery_setup["name"] = "discovery_setup" + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup) + ) + await hass.async_block_till_done() + + # check if both entities were setup correctly + config_setup_entity = hass.states.get(f"{domain}.config_setup") + assert config_setup_entity + + discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup") + assert discovery_setup_entity + + await help_test_unload_config_entry(hass, tmp_path, config_setup) + + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup) + ) + await hass.async_block_till_done() + + # check if both entities were unloaded correctly + config_setup_entity = hass.states.get(f"{domain}.{config_name}") + assert config_setup_entity is None + + discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup") + assert discovery_setup_entity is None diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 208a6ce2d61..e40397fd1d4 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -145,12 +145,17 @@ async def test_manual_config_starts_discovery_flow( async def test_manual_config_set( - hass, mock_try_connection, mock_finish_setup, mqtt_client_mock + hass, + mock_try_connection, + mock_finish_setup, + mqtt_client_mock, ): """Test manual config does not create an entry, and entry can be setup late.""" # MQTT config present in yaml config assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}}) await hass.async_block_till_done() + # do not try to reload + del hass.data["mqtt_reload_needed"] assert len(mock_finish_setup.mock_calls) == 0 mock_try_connection.return_value = True diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index c0d63cec1b4..3f85d4e89b1 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -73,6 +73,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -3364,3 +3365,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = cover.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index a9eb9b20825..6708703ddbb 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,13 +1,20 @@ """The tests for the MQTT device tracker platform using configuration.yaml.""" +import json from unittest.mock import patch import pytest from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH +from homeassistant.config_entries import ConfigEntryDisabler from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, Platform from homeassistant.setup import async_setup_component -from .test_common import help_test_setup_manual_entity_from_yaml +from .test_common import ( + MockConfigEntry, + help_test_entry_reload_with_new_config, + help_test_setup_manual_entity_from_yaml, + help_test_unload_config_entry, +) from tests.common import async_fire_mqtt_message @@ -265,3 +272,114 @@ async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): await help_test_setup_manual_entity_from_yaml(hass, DOMAIN, config) assert hass.states.get(entity_id) is not None + + +async def test_unload_entry( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path +): + """Test unloading the config entry.""" + # setup through configuration.yaml + await mqtt_mock_entry_no_yaml_config() + dev_id = "jan" + entity_id = f"{DOMAIN}.{dev_id}" + topic = "/location/jan" + location = "home" + + hass.config.components = {"mqtt", "zone"} + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} + ) + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == location + + # setup through discovery + dev_id = "piet" + subscription = "/location/#" + domain = DOMAIN + discovery_config = { + "devices": {dev_id: subscription}, + "state_topic": "some-state", + "name": "piet", + } + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_config) + ) + await hass.async_block_till_done() + + # check that both entities were created + config_setup_entity = hass.states.get(f"{domain}.jan") + assert config_setup_entity + + discovery_setup_entity = hass.states.get(f"{domain}.piet") + assert discovery_setup_entity + + await help_test_unload_config_entry(hass, tmp_path, {}) + await hass.async_block_till_done() + + # check that both entities were unsubscribed and that the location was not processed + async_fire_mqtt_message(hass, "some-state", "not_home") + async_fire_mqtt_message(hass, "location/jan", "not_home") + await hass.async_block_till_done() + + config_setup_entity = hass.states.get(f"{domain}.jan") + assert config_setup_entity.state == location + + # the discovered tracker is an entity which state is removed at unload + discovery_setup_entity = hass.states.get(f"{domain}.piet") + assert discovery_setup_entity is None + + +async def test_reload_entry_legacy( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path +): + """Test reloading the config entry with manual MQTT items.""" + # setup through configuration.yaml + await mqtt_mock_entry_no_yaml_config() + entity_id = f"{DOMAIN}.jan" + topic = "location/jan" + location = "home" + + config = { + DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + } + hass.config.components = {"mqtt", "zone"} + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == location + + await help_test_entry_reload_with_new_config(hass, tmp_path, config) + await hass.async_block_till_done() + + location = "not_home" + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == location + + +async def test_setup_with_disabled_entry( + hass, mock_device_tracker_conf, caplog +) -> None: + """Test setting up the platform with a disabled config entry.""" + # Try to setup the platform with a disabled config entry + config_entry = MockConfigEntry( + domain="mqtt", data={}, disabled_by=ConfigEntryDisabler.USER + ) + config_entry.add_to_hass(hass) + topic = "location/jan" + + config = { + DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + } + hass.config.components = {"mqtt", "zone"} + + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + assert ( + "MQTT device trackers will be not available until the config entry is enabled" + in caplog.text + ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 842e1dc4106..37a59ef6b53 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -12,6 +12,8 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component +from .test_common import help_test_unload_config_entry + from tests.common import ( assert_lists_same, async_fire_mqtt_message, @@ -1372,3 +1374,53 @@ async def test_trigger_debug_info(hass, mqtt_mock_entry_no_yaml_config): == "homeassistant/device_automation/bla2/config" ) assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config2 + + +async def test_unload_entry(hass, calls, device_reg, mqtt_mock, tmp_path) -> None: + """Test unloading the MQTT entry.""" + + data1 = ( + '{ "automation_type":"trigger",' + ' "device":{"identifiers":["0AFFD2"]},' + ' "topic": "foobar/triggers/button1",' + ' "type": "button_short_press",' + ' "subtype": "button_1" }' + ) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "bla1", + "type": "button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press")}, + }, + }, + ] + }, + ) + + # Fake short press 1 + async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") + await hass.async_block_till_done() + assert len(calls) == 1 + + await help_test_unload_config_entry(hass, tmp_path, {}) + + # Fake short press 2 + async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") + await hass.async_block_till_done() + assert len(calls) == 1 diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index b9ca5e3888d..37dcefc9d3f 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -58,6 +58,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1910,3 +1911,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = fan.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 0301e9e0481..38dc634578f 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -60,6 +60,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1296,3 +1297,12 @@ async def test_config_schema_validation(hass): CONFIG_SCHEMA({DOMAIN: {platform: [config]}}) with pytest.raises(MultipleInvalid): CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}}) + + +async def test_unload_config_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = humidifier.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b435798c241..de63528a08b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -17,6 +17,7 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, EVENT_HOMEASSISTANT_STARTED, @@ -32,7 +33,10 @@ from homeassistant.helpers.entity import Entity from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .test_common import help_test_setup_manual_entity_from_yaml +from .test_common import ( + help_test_entry_reload_with_new_config, + help_test_setup_manual_entity_from_yaml, +) from tests.common import ( MockConfigEntry, @@ -106,6 +110,18 @@ def record_calls(calls): return record_calls +@pytest.fixture +def empty_mqtt_config(hass, tmp_path): + """Fixture to provide an empty config from yaml.""" + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config_file.write_text("") + + with patch.object( + hass_config, "YAML_CONFIG_FILE", new_yaml_config_file + ) as empty_config: + yield empty_config + + async def test_mqtt_connects_on_home_assistant_mqtt_setup( hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): @@ -115,14 +131,14 @@ async def test_mqtt_connects_on_home_assistant_mqtt_setup( async def test_mqtt_disconnects_on_home_assistant_stop( - hass, mqtt_mock_entry_no_yaml_config + hass, mqtt_mock_entry_no_yaml_config, mqtt_client_mock ): """Test if client stops on HA stop.""" - mqtt_mock = await mqtt_mock_entry_no_yaml_config() + await mqtt_mock_entry_no_yaml_config() hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() - assert mqtt_mock.async_disconnect.called + assert mqtt_client_mock.loop_stop.call_count == 1 async def test_publish(hass, mqtt_mock_entry_no_yaml_config): @@ -521,8 +537,11 @@ async def test_service_call_with_ascii_qos_retain_flags( assert not mqtt_mock.async_publish.call_args[0][3] -async def test_publish_function_with_bad_encoding_conditions(hass, caplog): - """Test internal publish function with bas use cases.""" +async def test_publish_function_with_bad_encoding_conditions( + hass, caplog, mqtt_mock_entry_no_yaml_config +): + """Test internal publish function with basic use cases.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_publish( hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None ) @@ -1249,13 +1268,18 @@ async def test_restore_all_active_subscriptions_on_reconnect( assert mqtt_client_mock.subscribe.mock_calls == expected -async def test_initial_setup_logs_error(hass, caplog, mqtt_client_mock): +async def test_initial_setup_logs_error( + hass, caplog, mqtt_client_mock, empty_mqtt_config +): """Test for setup failure if initial client connection fails.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - + entry.add_to_hass(hass) mqtt_client_mock.connect.return_value = 1 - assert await mqtt.async_setup_entry(hass, entry) - await hass.async_block_till_done() + try: + assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() + except HomeAssistantError: + assert True assert "Failed to connect to MQTT server:" in caplog.text @@ -1298,6 +1322,7 @@ async def test_handle_mqtt_on_callback( async def test_publish_error(hass, caplog): """Test publish error.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) # simulate an Out of memory error with patch("paho.mqtt.client.Client") as mock_client: @@ -1365,6 +1390,7 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker", "password": "somepassword"}, ) + entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: mock_client().username_pw_set = mock_usename_password_set @@ -1429,9 +1455,11 @@ async def test_setup_mqtt_client_protocol(hass): mqtt.config_integration.CONF_PROTOCOL: "3.1", }, ) + entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: mock_client.on_connect(return_value=0) assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() # check if protocol setup was correctly assert mock_client.call_args[1]["protocol"] == 3 @@ -1467,15 +1495,18 @@ async def test_handle_mqtt_timeout_on_callback(hass, caplog): entry = MockConfigEntry( domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} ) - # Set up the integration - assert await mqtt.async_setup_entry(hass, entry) + entry.add_to_hass(hass) + # Make sure we are connected correctly mock_client.on_connect(mock_client, None, None, 0) + # Set up the integration + assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() # Now call we publish without simulating and ACK callback await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") await hass.async_block_till_done() - # The is no ACK so we should see a timeout in the log after publishing + # There is no ACK so we should see a timeout in the log after publishing assert len(mock_client.publish.mock_calls) == 1 assert "No ACK from MQTT server" in caplog.text @@ -1483,10 +1514,12 @@ async def test_handle_mqtt_timeout_on_callback(hass, caplog): async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplog): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = MagicMock(side_effect=OSError("Connection error")) assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() assert "Failed to connect to MQTT server due to exception:" in caplog.text @@ -1514,8 +1547,9 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( domain=mqtt.DOMAIN, data=config_item_data, ) - + entry.add_to_hass(hass) assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() assert calls @@ -1546,8 +1580,9 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): domain=mqtt.DOMAIN, data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, ) - + entry.add_to_hass(hass) assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() assert calls @@ -2644,3 +2679,206 @@ async def test_config_schema_validation(hass): config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}} with pytest.raises(vol.MultipleInvalid): CONFIG_SCHEMA(config) + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_unload_config_entry( + hass, mqtt_mock, mqtt_client_mock, tmp_path, caplog +) -> None: + """Test unloading the MQTT entry.""" + assert hass.services.has_service(mqtt.DOMAIN, "dump") + assert hass.services.has_service(mqtt.DOMAIN, "publish") + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + # Publish just before unloading to test await cleanup + mqtt_client_mock.reset_mock() + mqtt.publish(hass, "just_in_time", "published", qos=0, retain=False) + + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({}) + new_yaml_config_file.write_text(new_yaml_config) + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + mqtt_client_mock.publish.assert_any_call("just_in_time", "published", 0, False) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + await hass.async_block_till_done() + assert not hass.services.has_service(mqtt.DOMAIN, "dump") + assert not hass.services.has_service(mqtt.DOMAIN, "publish") + assert "No ACK from MQTT server" not in caplog.text + + +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_setup_with_disabled_entry(hass, caplog) -> None: + """Test setting up the platform with a disabled config entry.""" + # Try to setup the platform with a disabled config entry + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={}, disabled_by=ConfigEntryDisabler.USER + ) + config_entry.add_to_hass(hass) + + config = {mqtt.DOMAIN: {}} + await async_setup_component(hass, mqtt.DOMAIN, config) + await hass.async_block_till_done() + + assert "MQTT will be not available until the config entry is enabled" in caplog.text + + +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_publish_or_subscribe_without_valid_config_entry(hass, caplog): + """Test internal publish function with bas use cases.""" + with pytest.raises(HomeAssistantError): + await mqtt.async_publish( + hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None + ) + with pytest.raises(HomeAssistantError): + await mqtt.async_subscribe(hass, "some-topic", lambda: None, qos=0) + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_reload_entry_with_new_config(hass, tmp_path): + """Test reloading the config entry with a new yaml config.""" + config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + config_yaml_new = { + "mqtt": { + "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] + }, + "light": [ + { + "platform": "mqtt", + "name": "test_new_legacy", + "command_topic": "test-topic_new", + } + ], + } + await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + assert hass.states.get("light.test_old1") is not None + + await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new) + assert hass.states.get("light.test_old1") is None + assert hass.states.get("light.test_new_modern") is not None + assert hass.states.get("light.test_new_legacy") is not None + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): + """Test disabling and enabling the config entry.""" + config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + config_yaml_new = { + "mqtt": { + "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] + }, + "light": [ + { + "platform": "mqtt", + "name": "test_new_legacy", + "command_topic": "test-topic_new", + } + ], + } + await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + assert hass.states.get("light.test_old1") is not None + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + + assert mqtt_config_entry.state is ConfigEntryState.LOADED + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump(config_yaml_new) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file), patch( + "paho.mqtt.client.Client" + ) as mock_client: + mock_client().connect = lambda *args: 0 + + # Late discovery of a light + config = '{"name": "abc", "command_topic": "test-topic"}' + async_fire_mqtt_message(hass, "homeassistant/light/abc/config", config) + + # Disable MQTT config entry + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, ConfigEntryDisabler.USER + ) + + await hass.async_block_till_done() + await hass.async_block_till_done() + # Assert that the discovery was still received + # but kipped the setup + assert ( + "MQTT integration is disabled, skipping setup of manually configured MQTT light" + in caplog.text + ) + + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + assert hass.states.get("light.test_old1") is None + + # Enable the entry again + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, None + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + assert hass.states.get("light.test_old1") is None + assert hass.states.get("light.test_new_modern") is not None + assert hass.states.get("light.test_new_legacy") is not None + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +@pytest.mark.parametrize( + "config, unique", + [ + ( + [ + { + "name": "test1", + "unique_id": "very_not_unique_deadbeef", + "command_topic": "test-topic_unique", + }, + { + "name": "test2", + "unique_id": "very_not_unique_deadbeef", + "command_topic": "test-topic_unique", + }, + ], + False, + ), + ( + [ + { + "name": "test1", + "unique_id": "very_unique_deadbeef1", + "command_topic": "test-topic_unique", + }, + { + "name": "test2", + "unique_id": "very_unique_deadbeef2", + "command_topic": "test-topic_unique", + }, + ], + True, + ), + ], +) +async def test_setup_manual_items_with_unique_ids( + hass, tmp_path, caplog, config, unique +): + """Test setup manual items is generating unique id's.""" + await help_test_setup_manual_entity_from_yaml(hass, "light", config) + + assert hass.states.get("light.test1") is not None + assert (hass.states.get("light.test2") is not None) == unique + assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique + + # reload and assert again + caplog.clear() + await help_test_entry_reload_with_new_config( + hass, tmp_path, {"mqtt": {"light": config}} + ) + + assert hass.states.get("light.test1") is not None + assert (hass.states.get("light.test2") is not None) == unique + assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 4d8d8f24a3c..bfafc99a9e2 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -240,6 +240,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -3803,3 +3804,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = light.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 6e271d08651..2c96468057f 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -72,6 +72,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1266,3 +1267,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = light.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 1bf4183e60f..f6dc4a0ed6d 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -48,6 +48,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -748,3 +749,12 @@ async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = LOCK_DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 1db7c5e3463..458f1f740e1 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -57,6 +57,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -853,3 +854,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = number.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 3036565dad5..713410059fe 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -22,6 +22,7 @@ from .test_common import ( help_test_reloadable_late, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, ) DEFAULT_CONFIG = { @@ -237,3 +238,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = scene.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index c22bd43b86f..4c3a0523951 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -48,6 +48,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -687,3 +688,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = select.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index f30bcf43392..ab094c20b6f 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -58,6 +58,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1213,3 +1214,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = sensor.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 6da9682c1c7..13648f1c486 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -45,6 +45,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -975,3 +976,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = siren.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index ba23efc859c..af6c0f99f50 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -42,6 +42,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -664,3 +665,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = switch.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index f06dd6f5244..507c6d99bed 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -11,6 +11,8 @@ from homeassistant.const import Platform from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component +from .test_common import help_test_unload_config_entry + from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -797,3 +799,28 @@ async def test_cleanup_device_with_entity2( # Verify device registry entry is cleared device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None + + +async def test_unload_entry(hass, device_reg, mqtt_mock, tag_mock, tmp_path) -> None: + """Test unloading the MQTT entry.""" + + config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) + + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + + # Fake tag scan, should be processed + async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) + await hass.async_block_till_done() + tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) + + tag_mock.reset_mock() + + await help_test_unload_config_entry(hass, tmp_path, {}) + await hass.async_block_till_done() + + # Fake tag scan, should not be processed + async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) + await hass.async_block_till_done() + tag_mock.assert_not_called() From ff1cdb4de7522e04ddb8607c1da11386dee228e6 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Tue, 12 Jul 2022 12:03:26 +0200 Subject: [PATCH 396/820] Bump afsapi to 0.2.6 (#75041) --- homeassistant/components/frontier_silicon/manifest.json | 2 +- homeassistant/components/frontier_silicon/media_player.py | 7 +++++-- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 12fb5145aa0..b04d68e672d 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.2.5"], + "requirements": ["afsapi==0.2.6"], "codeowners": ["@wlcrs"], "iot_class": "local_polling" } diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 61dc6e69726..b16374c0bc0 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -179,11 +179,14 @@ class AFSAPIDevice(MediaPlayerEntity): self._attr_media_artist = await afsapi.get_play_artist() self._attr_media_album_name = await afsapi.get_play_album() - self._attr_source = (await afsapi.get_mode()).label + radio_mode = await afsapi.get_mode() + self._attr_source = radio_mode.label if radio_mode is not None else None self._attr_is_volume_muted = await afsapi.get_mute() self._attr_media_image_url = await afsapi.get_play_graphic() - self._attr_sound_mode = (await afsapi.get_eq_preset()).label + + eq_preset = await afsapi.get_eq_preset() + self._attr_sound_mode = eq_preset.label if eq_preset is not None else None volume = await self.fs_device.get_volume() diff --git a/requirements_all.txt b/requirements_all.txt index 933ca47b086..597efbc799b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,7 +89,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.2.5 +afsapi==0.2.6 # homeassistant.components.agent_dvr agent-py==0.0.23 From cd25bc19013e9b981d4afeaefef85d4a4da00c5e Mon Sep 17 00:00:00 2001 From: hahn-th Date: Tue, 12 Jul 2022 15:31:04 +0200 Subject: [PATCH 397/820] Bump homematicip to 1.0.4 (#75053) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 40f7e67fd07..f6fb9f3e739 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.3"], + "requirements": ["homematicip==1.0.4"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 597efbc799b..53823fbc756 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -837,7 +837,7 @@ home-assistant-frontend==20220707.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.3 +homematicip==1.0.4 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d94346b384..193daabeabc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,7 +607,7 @@ home-assistant-frontend==20220707.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.3 +homematicip==1.0.4 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From 7283d1b7fbb715119a9ef94c9c0c4378e5a266e0 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 12 Jul 2022 15:35:12 +0200 Subject: [PATCH 398/820] Migrate Brother to new entity naming style (#75000) --- homeassistant/components/brother/sensor.py | 51 +++++++++++----------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index a589ea0bd77..b6af96087af 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -111,6 +111,8 @@ async def async_setup_entry( class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): """Define an Brother Printer sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: BrotherDataUpdateCoordinator, @@ -121,7 +123,6 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) self._attrs: dict[str, Any] = {} self._attr_device_info = device_info - self._attr_name = f"{coordinator.data.model} {description.name}" self._attr_unique_id = f"{coordinator.data.serial.lower()}_{description.key}" self.entity_description = description @@ -168,13 +169,13 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_STATUS, icon="mdi:printer", - name=ATTR_STATUS.title(), + name="Status", entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_PAGE_COUNTER, icon="mdi:file-document-outline", - name=ATTR_PAGE_COUNTER.replace("_", " ").title(), + name="Page counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -182,7 +183,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BW_COUNTER, icon="mdi:file-document-outline", - name=ATTR_BW_COUNTER.replace("_", " ").title(), + name="B/W counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -190,7 +191,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_COLOR_COUNTER, icon="mdi:file-document-outline", - name=ATTR_COLOR_COUNTER.replace("_", " ").title(), + name="Color counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -198,7 +199,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_DUPLEX_COUNTER, icon="mdi:file-document-outline", - name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), + name="Duplex unit pages counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -206,7 +207,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -214,7 +215,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BLACK_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Black drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -222,7 +223,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_CYAN_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Cyan drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -230,7 +231,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Magenta drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -238,7 +239,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_YELLOW_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Yellow drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -246,7 +247,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BELT_UNIT_REMAINING_LIFE, icon="mdi:current-ac", - name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), + name="Belt unit remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -254,7 +255,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_FUSER_REMAINING_LIFE, icon="mdi:water-outline", - name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), + name="Fuser remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -262,7 +263,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_LASER_REMAINING_LIFE, icon="mdi:spotlight-beam", - name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), + name="Laser remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -270,7 +271,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_PF_KIT_1_REMAINING_LIFE, icon="mdi:printer-3d", - name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), + name="PF Kit 1 remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -278,7 +279,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_PF_KIT_MP_REMAINING_LIFE, icon="mdi:printer-3d", - name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), + name="PF Kit MP remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -286,7 +287,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BLACK_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), + name="Black toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -294,7 +295,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_CYAN_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), + name="Cyan toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -302,7 +303,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_MAGENTA_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), + name="Magenta toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -310,7 +311,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_YELLOW_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), + name="Yellow toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -318,7 +319,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BLACK_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), + name="Black ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -326,7 +327,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_CYAN_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), + name="Cyan ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -334,7 +335,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_MAGENTA_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), + name="Magenta ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -342,14 +343,14 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_YELLOW_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), + name="Yellow ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_UPTIME, - name=ATTR_UPTIME.title(), + name="Uptime", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, From 5fdae0fc5bbf574bc8391e738a9b0285f3c9a095 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Tue, 12 Jul 2022 15:56:16 +0200 Subject: [PATCH 399/820] Migrate HomeWizard to new entity naming style (#74958) --- homeassistant/components/homewizard/sensor.py | 33 +++++++-------- homeassistant/components/homewizard/switch.py | 9 ++--- tests/components/homewizard/test_sensor.py | 34 ++++++++-------- tests/components/homewizard/test_switch.py | 40 ++++++------------- 4 files changed, 50 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index de39ce6f242..a66a2664ae1 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -31,25 +31,25 @@ _LOGGER = logging.getLogger(__name__) SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( key="smr_version", - name="DSMR Version", + name="DSMR version", icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="meter_model", - name="Smart Meter Model", + name="Smart meter model", icon="mdi:gauge", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="wifi_ssid", - name="Wifi SSID", + name="Wi-Fi SSID", icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="wifi_strength", - name="Wifi Strength", + name="Wi-Fi strength", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -58,77 +58,77 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( ), SensorEntityDescription( key="total_power_import_t1_kwh", - name="Total Power Import T1", + name="Total power import T1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="total_power_import_t2_kwh", - name="Total Power Import T2", + name="Total power import T2", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="total_power_export_t1_kwh", - name="Total Power Export T1", + name="Total power export T1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="total_power_export_t2_kwh", - name="Total Power Export T2", + name="Total power export T2", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="active_power_w", - name="Active Power", + name="Active power", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="active_power_l1_w", - name="Active Power L1", + name="Active power L1", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="active_power_l2_w", - name="Active Power L2", + name="Active power L2", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="active_power_l3_w", - name="Active Power L3", + name="Active power L3", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="total_gas_m3", - name="Total Gas", + name="Total gas", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="active_liter_lpm", - name="Active Water Usage", + name="Active water usage", native_unit_of_measurement="l/min", icon="mdi:water", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="total_liter_m3", - name="Total Water Usage", + name="Total water usage", native_unit_of_measurement=VOLUME_CUBIC_METERS, icon="mdi:gauge", state_class=SensorStateClass.TOTAL_INCREASING, @@ -153,6 +153,8 @@ async def async_setup_entry( class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorEntity): """Representation of a HomeWizard Sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -166,7 +168,6 @@ class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorE self.entry = entry # Config attributes. - self._attr_name = f"{entry.title} {description.name}" self.data_type = description.key self._attr_unique_id = f"{entry.unique_id}_{description.key}" diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index eb2e9c49afe..eca8a7670be 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -36,6 +36,8 @@ class HWEnergySwitchEntity( ): """Representation switchable entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -65,9 +67,6 @@ class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): """Initialize the switch.""" super().__init__(coordinator, entry, "power_on") - # Config attributes - self._attr_name = f"{entry.title} Switch" - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.coordinator.api.state_set(power_on=True) @@ -101,6 +100,7 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): It disables any method that can turn of the relay. """ + _attr_name = "Switch lock" _attr_device_class = SwitchDeviceClass.SWITCH _attr_entity_category = EntityCategory.CONFIG @@ -110,9 +110,6 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): """Initialize the switch.""" super().__init__(coordinator, entry, "switch_lock") - # Config attributes - self._attr_name = f"{entry.title} Switch Lock" - async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch-lock on.""" await self.coordinator.api.state_set(switch_lock=True) diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index a81291861a2..85b6dc58235 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -61,7 +61,7 @@ async def test_sensor_entity_smr_version( assert state.state == "50" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) DSMR Version" + == "Product Name (aabbccddeeff) DSMR version" ) assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -101,7 +101,7 @@ async def test_sensor_entity_meter_model( assert state.state == "Model X" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Smart Meter Model" + == "Product Name (aabbccddeeff) Smart meter model" ) assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -128,8 +128,8 @@ async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config entity_registry = er.async_get(hass) - state = hass.states.get("sensor.product_name_aabbccddeeff_wifi_ssid") - entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wifi_ssid") + state = hass.states.get("sensor.product_name_aabbccddeeff_wi_fi_ssid") + entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wi_fi_ssid") assert entry assert state assert entry.unique_id == "aabbccddeeff_wifi_ssid" @@ -137,7 +137,7 @@ async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config assert state.state == "My Wifi" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Wifi SSID" + == "Product Name (aabbccddeeff) Wi-Fi SSID" ) assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -166,7 +166,7 @@ async def test_sensor_entity_wifi_strength( entity_registry = er.async_get(hass) - entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wifi_strength") + entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wi_fi_strength") assert entry assert entry.unique_id == "aabbccddeeff_wifi_strength" assert entry.disabled @@ -206,7 +206,7 @@ async def test_sensor_entity_total_power_import_t1_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Import T1" + == "Product Name (aabbccddeeff) Total power import T1" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -248,7 +248,7 @@ async def test_sensor_entity_total_power_import_t2_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Import T2" + == "Product Name (aabbccddeeff) Total power import T2" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -290,7 +290,7 @@ async def test_sensor_entity_total_power_export_t1_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Export T1" + == "Product Name (aabbccddeeff) Total power export T1" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -332,7 +332,7 @@ async def test_sensor_entity_total_power_export_t2_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Export T2" + == "Product Name (aabbccddeeff) Total power export T2" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -370,7 +370,7 @@ async def test_sensor_entity_active_power( assert state.state == "123.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power" + == "Product Name (aabbccddeeff) Active power" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -410,7 +410,7 @@ async def test_sensor_entity_active_power_l1( assert state.state == "123.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power L1" + == "Product Name (aabbccddeeff) Active power L1" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -450,7 +450,7 @@ async def test_sensor_entity_active_power_l2( assert state.state == "456.456" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power L2" + == "Product Name (aabbccddeeff) Active power L2" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -490,7 +490,7 @@ async def test_sensor_entity_active_power_l3( assert state.state == "789.789" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power L3" + == "Product Name (aabbccddeeff) Active power L3" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -526,7 +526,7 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config assert state.state == "50" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Gas" + == "Product Name (aabbccddeeff) Total gas" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS @@ -566,7 +566,7 @@ async def test_sensor_entity_active_liters( assert state.state == "12.345" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Water Usage" + == "Product Name (aabbccddeeff) Active water usage" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -607,7 +607,7 @@ async def test_sensor_entity_total_liters( assert state.state == "1234.567" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Water Usage" + == "Product Name (aabbccddeeff) Total water usage" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index 118f0774a47..224d32e1b5c 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -39,7 +39,7 @@ async def test_switch_entity_not_loaded_when_not_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state_power_on = hass.states.get("sensor.product_name_aabbccddeeff_switch") + state_power_on = hass.states.get("sensor.product_name_aabbccddeeff") state_switch_lock = hass.states.get("sensor.product_name_aabbccddeeff_switch_lock") assert state_power_on is None @@ -67,10 +67,8 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e entity_registry = er.async_get(hass) - state_power_on = hass.states.get("switch.product_name_aabbccddeeff_switch") - entry_power_on = entity_registry.async_get( - "switch.product_name_aabbccddeeff_switch" - ) + state_power_on = hass.states.get("switch.product_name_aabbccddeeff") + entry_power_on = entity_registry.async_get("switch.product_name_aabbccddeeff") assert state_power_on assert entry_power_on assert entry_power_on.unique_id == "aabbccddeeff_power_on" @@ -78,7 +76,7 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e assert state_power_on.state == STATE_OFF assert ( state_power_on.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Switch" + == "Product Name (aabbccddeeff)" ) assert state_power_on.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_OUTLET assert ATTR_ICON not in state_power_on.attributes @@ -95,7 +93,7 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e assert state_switch_lock.state == STATE_OFF assert ( state_switch_lock.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Switch Lock" + == "Product Name (aabbccddeeff) Switch lock" ) assert state_switch_lock.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SWITCH assert ATTR_ICON not in state_switch_lock.attributes @@ -127,38 +125,30 @@ async def test_switch_power_on_off(hass, mock_config_entry_data, mock_config_ent await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state - == STATE_OFF - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_OFF # Turn power_on on await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, - {"entity_id": "switch.product_name_aabbccddeeff_switch"}, + {"entity_id": "switch.product_name_aabbccddeeff"}, blocking=True, ) await hass.async_block_till_done() assert len(api.state_set.mock_calls) == 1 - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_ON # Turn power_on off await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_OFF, - {"entity_id": "switch.product_name_aabbccddeeff_switch"}, + {"entity_id": "switch.product_name_aabbccddeeff"}, blocking=True, ) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state - == STATE_OFF - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_OFF assert len(api.state_set.mock_calls) == 2 @@ -254,9 +244,7 @@ async def test_switch_lock_sets_power_on_unavailable( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_ON assert ( hass.states.get("switch.product_name_aabbccddeeff_switch_lock").state == STATE_OFF @@ -273,7 +261,7 @@ async def test_switch_lock_sets_power_on_unavailable( await hass.async_block_till_done() assert len(api.state_set.mock_calls) == 1 assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state + hass.states.get("switch.product_name_aabbccddeeff").state == STATE_UNAVAILABLE ) assert ( @@ -290,9 +278,7 @@ async def test_switch_lock_sets_power_on_unavailable( ) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_ON assert ( hass.states.get("switch.product_name_aabbccddeeff_switch_lock").state == STATE_OFF From 5489b2111a21738155edc42168029584c8b28f6a Mon Sep 17 00:00:00 2001 From: Gabe Cook Date: Tue, 12 Jul 2022 09:06:38 -0500 Subject: [PATCH 400/820] Fix Ruckus Unleashed SSH connection failures (#75032) --- .../components/ruckus_unleashed/__init__.py | 7 +++---- .../components/ruckus_unleashed/config_flow.py | 14 +++++++------- .../components/ruckus_unleashed/coordinator.py | 4 +--- .../components/ruckus_unleashed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ruckus_unleashed/test_init.py | 4 ++-- 7 files changed, 16 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 258a5968ab1..5861486457f 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -29,8 +29,7 @@ from .coordinator import RuckusUnleashedDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ruckus Unleashed from a config entry.""" try: - ruckus = await hass.async_add_executor_job( - Ruckus, + ruckus = await Ruckus.create( entry.data[CONF_HOST], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], @@ -42,10 +41,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - system_info = await hass.async_add_executor_job(ruckus.system_info) + system_info = await ruckus.system_info() registry = device_registry.async_get(hass) - ap_info = await hass.async_add_executor_job(ruckus.ap_info) + ap_info = await ruckus.ap_info() for device in ap_info[API_AP][API_ID].values(): registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index 7d34e620a13..4adf245c3de 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -21,22 +21,24 @@ DATA_SCHEMA = vol.Schema( ) -def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ try: - ruckus = Ruckus(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) + ruckus = await Ruckus.create( + data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD] + ) except AuthenticationError as error: raise InvalidAuth from error except ConnectionError as error: raise CannotConnect from error - mesh_name = ruckus.mesh_name() + mesh_name = await ruckus.mesh_name() - system_info = ruckus.system_info() + system_info = await ruckus.system_info() try: host_serial = system_info[API_SYSTEM_OVERVIEW][API_SERIAL] except KeyError as error: @@ -58,9 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: try: - info = await self.hass.async_add_executor_job( - validate_input, self.hass, user_input - ) + info = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: diff --git a/homeassistant/components/ruckus_unleashed/coordinator.py b/homeassistant/components/ruckus_unleashed/coordinator.py index 8b80eaae0da..e84b79ef843 100644 --- a/homeassistant/components/ruckus_unleashed/coordinator.py +++ b/homeassistant/components/ruckus_unleashed/coordinator.py @@ -37,9 +37,7 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator): async def _fetch_clients(self) -> dict: """Fetch clients from the API and format them.""" - clients = await self.hass.async_add_executor_job( - self.ruckus.current_active_clients - ) + clients = await self.ruckus.current_active_clients() return {e[API_MAC]: e for e in clients[API_CURRENT_ACTIVE_CLIENTS][API_CLIENTS]} async def _async_update_data(self) -> dict: diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index f010d340147..a6b2ad0271c 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -3,7 +3,7 @@ "name": "Ruckus Unleashed", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", - "requirements": ["pyruckus==0.12"], + "requirements": ["pyruckus==0.16"], "codeowners": ["@gabe565"], "iot_class": "local_polling", "loggers": ["pexpect", "pyruckus"] diff --git a/requirements_all.txt b/requirements_all.txt index 53823fbc756..36b3ba5cb62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1783,7 +1783,7 @@ pyrisco==0.3.1 pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed -pyruckus==0.12 +pyruckus==0.16 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 193daabeabc..e4e8b4262f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1217,7 +1217,7 @@ pyrisco==0.3.1 pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed -pyruckus==0.12 +pyruckus==0.16 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index e9ac9ec7cd8..d72856aa542 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -31,7 +31,7 @@ async def test_setup_entry_login_error(hass): """Test entry setup failed due to login error.""" entry = mock_config_entry() with patch( - "homeassistant.components.ruckus_unleashed.Ruckus", + "homeassistant.components.ruckus_unleashed.Ruckus.connect", side_effect=AuthenticationError, ): entry.add_to_hass(hass) @@ -45,7 +45,7 @@ async def test_setup_entry_connection_error(hass): """Test entry setup failed due to connection error.""" entry = mock_config_entry() with patch( - "homeassistant.components.ruckus_unleashed.Ruckus", + "homeassistant.components.ruckus_unleashed.Ruckus.connect", side_effect=ConnectionError, ): entry.add_to_hass(hass) From cf612c4bec584f6627eba17e551879e0df67457f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Tue, 12 Jul 2022 17:32:37 +0300 Subject: [PATCH 401/820] Migrate Vallox to new entity naming style (#75025) --- .../components/vallox/binary_sensor.py | 4 ++-- homeassistant/components/vallox/fan.py | 2 +- homeassistant/components/vallox/sensor.py | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/vallox/binary_sensor.py b/homeassistant/components/vallox/binary_sensor.py index 762b63c0c1d..9f1b3018186 100644 --- a/homeassistant/components/vallox/binary_sensor.py +++ b/homeassistant/components/vallox/binary_sensor.py @@ -21,6 +21,7 @@ class ValloxBinarySensor(ValloxEntity, BinarySensorEntity): entity_description: ValloxBinarySensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True def __init__( self, @@ -33,7 +34,6 @@ class ValloxBinarySensor(ValloxEntity, BinarySensorEntity): self.entity_description = description - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{self._device_uuid}-{description.key}" @property @@ -59,7 +59,7 @@ class ValloxBinarySensorEntityDescription( SENSORS: tuple[ValloxBinarySensorEntityDescription, ...] = ( ValloxBinarySensorEntityDescription( key="post_heater", - name="Post Heater", + name="Post heater", icon="mdi:radiator", metric_key="A_CYC_IO_HEATER", ), diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 4ba7d2d88fd..be496bbf899 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -83,6 +83,7 @@ class ValloxFan(ValloxEntity, FanEntity): """Representation of the fan.""" _attr_supported_features = FanEntityFeature.PRESET_MODE + _attr_has_entity_name = True def __init__( self, @@ -95,7 +96,6 @@ class ValloxFan(ValloxEntity, FanEntity): self._client = client - self._attr_name = name self._attr_unique_id = str(self._device_uuid) @property diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 54a010c3e3d..48b44dd97fe 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -37,6 +37,7 @@ class ValloxSensor(ValloxEntity, SensorEntity): entity_description: ValloxSensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True def __init__( self, @@ -49,7 +50,6 @@ class ValloxSensor(ValloxEntity, SensorEntity): self.entity_description = description - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{self._device_uuid}-{description.key}" @property @@ -129,13 +129,13 @@ class ValloxSensorEntityDescription(SensorEntityDescription): SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ValloxSensorEntityDescription( key="current_profile", - name="Current Profile", + name="Current profile", icon="mdi:gauge", sensor_type=ValloxProfileSensor, ), ValloxSensorEntityDescription( key="fan_speed", - name="Fan Speed", + name="Fan speed", metric_key="A_CYC_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, @@ -144,20 +144,20 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="remaining_time_for_filter", - name="Remaining Time For Filter", + name="Remaining time for filter", device_class=SensorDeviceClass.TIMESTAMP, sensor_type=ValloxFilterRemainingSensor, ), ValloxSensorEntityDescription( key="cell_state", - name="Cell State", + name="Cell state", icon="mdi:swap-horizontal-bold", metric_key="A_CYC_CELL_STATE", sensor_type=ValloxCellStateSensor, ), ValloxSensorEntityDescription( key="extract_air", - name="Extract Air", + name="Extract air", metric_key="A_CYC_TEMP_EXTRACT_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -165,7 +165,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="exhaust_air", - name="Exhaust Air", + name="Exhaust air", metric_key="A_CYC_TEMP_EXHAUST_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -173,7 +173,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="outdoor_air", - name="Outdoor Air", + name="Outdoor air", metric_key="A_CYC_TEMP_OUTDOOR_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -181,7 +181,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="supply_air", - name="Supply Air", + name="Supply air", metric_key="A_CYC_TEMP_SUPPLY_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, From b0fde206b8b25f9fb7e7e24d07849c2b6b76ee83 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Jul 2022 19:26:06 +0200 Subject: [PATCH 402/820] Teach resolution center about fixing issues (#74694) --- .../components/resolution_center/__init__.py | 9 +- .../resolution_center/issue_handler.py | 77 ++++- .../resolution_center/issue_registry.py | 5 + .../resolution_center/manifest.json | 3 +- .../components/resolution_center/models.py | 17 ++ .../resolution_center/websocket_api.py | 78 +++++ .../components/resolution_center/test_init.py | 42 ++- .../resolution_center/test_issue_registry.py | 3 + .../resolution_center/test_websocket_api.py | 268 +++++++++++++++++- 9 files changed, 485 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/resolution_center/__init__.py b/homeassistant/components/resolution_center/__init__.py index 1446aa68bba..7d0cd8416c3 100644 --- a/homeassistant/components/resolution_center/__init__.py +++ b/homeassistant/components/resolution_center/__init__.py @@ -4,16 +4,19 @@ from __future__ import annotations from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import websocket_api +from . import issue_handler, websocket_api from .const import DOMAIN -from .issue_handler import async_create_issue, async_delete_issue +from .issue_handler import ResolutionCenterFlow, async_create_issue, async_delete_issue from .issue_registry import async_load as async_load_issue_registry -__all__ = ["DOMAIN", "async_create_issue", "async_delete_issue"] +__all__ = ["DOMAIN", "ResolutionCenterFlow", "async_create_issue", "async_delete_issue"] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Resolution Center.""" + hass.data[DOMAIN] = {} + + issue_handler.async_setup(hass) websocket_api.async_setup(hass) await async_load_issue_registry(hass) diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/resolution_center/issue_handler.py index 245895fa2db..210a998ce14 100644 --- a/homeassistant/components/resolution_center/issue_handler.py +++ b/homeassistant/components/resolution_center/issue_handler.py @@ -1,12 +1,85 @@ """The resolution center integration.""" from __future__ import annotations +from typing import Any + from awesomeversion import AwesomeVersion, AwesomeVersionStrategy +from homeassistant import data_entry_flow from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) +from .const import DOMAIN from .issue_registry import async_get as async_get_issue_registry -from .models import IssueSeverity +from .models import IssueSeverity, ResolutionCenterFlow, ResolutionCenterProtocol + + +class ResolutionCenterFlowManager(data_entry_flow.FlowManager): + """Manage resolution center flows.""" + + async def async_create_flow( + self, + handler_key: Any, + *, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> ResolutionCenterFlow: + """Create a flow. platform is a resolution center module.""" + if "platforms" not in self.hass.data[DOMAIN]: + await async_process_resolution_center_platforms(self.hass) + + platforms: dict[str, ResolutionCenterProtocol] = self.hass.data[DOMAIN][ + "platforms" + ] + if handler_key not in platforms: + raise data_entry_flow.UnknownHandler + platform = platforms[handler_key] + + assert data and "issue_id" in data + issue_id = data["issue_id"] + + issue_registry = async_get_issue_registry(self.hass) + issue = issue_registry.async_get_issue(handler_key, issue_id) + if issue is None or not issue.is_fixable: + raise data_entry_flow.UnknownStep + + return await platform.async_create_fix_flow(self.hass, issue_id) + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: + """Complete a fix flow.""" + async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"]) + if "result" not in result: + result["result"] = None + return result + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Initialize resolution center.""" + hass.data[DOMAIN]["flow_manager"] = ResolutionCenterFlowManager(hass) + + +async def async_process_resolution_center_platforms(hass: HomeAssistant) -> None: + """Start processing resolution center platforms.""" + hass.data[DOMAIN]["platforms"] = {} + + await async_process_integration_platforms( + hass, DOMAIN, _register_resolution_center_platform + ) + + +async def _register_resolution_center_platform( + hass: HomeAssistant, integration_domain: str, platform: ResolutionCenterProtocol +) -> None: + """Register a resolution center platform.""" + if not hasattr(platform, "async_create_fix_flow"): + raise HomeAssistantError(f"Invalid resolution center platform {platform}") + hass.data[DOMAIN]["platforms"][integration_domain] = platform @callback @@ -16,6 +89,7 @@ def async_create_issue( issue_id: str, *, breaks_in_ha_version: str | None = None, + is_fixable: bool, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -35,6 +109,7 @@ def async_create_issue( domain, issue_id, breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py index 7d5bbb482ba..d9042d891dd 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -25,6 +25,7 @@ class IssueEntry: breaks_in_ha_version: str | None dismissed_version: str | None domain: str + is_fixable: bool | None issue_id: str learn_more_url: str | None severity: IssueSeverity | None @@ -55,6 +56,7 @@ class IssueRegistry: issue_id: str, *, breaks_in_ha_version: str | None = None, + is_fixable: bool, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -68,6 +70,7 @@ class IssueRegistry: breaks_in_ha_version=breaks_in_ha_version, dismissed_version=None, domain=domain, + is_fixable=is_fixable, issue_id=issue_id, learn_more_url=learn_more_url, severity=severity, @@ -81,6 +84,7 @@ class IssueRegistry: issue, active=True, breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -127,6 +131,7 @@ class IssueRegistry: breaks_in_ha_version=None, dismissed_version=issue["dismissed_version"], domain=issue["domain"], + is_fixable=None, issue_id=issue["issue_id"], learn_more_url=None, severity=None, diff --git a/homeassistant/components/resolution_center/manifest.json b/homeassistant/components/resolution_center/manifest.json index 87cd309ad3d..4b8c1bb2506 100644 --- a/homeassistant/components/resolution_center/manifest.json +++ b/homeassistant/components/resolution_center/manifest.json @@ -3,5 +3,6 @@ "name": "Resolution Center", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/resolution_center", - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "dependencies": ["http"] } diff --git a/homeassistant/components/resolution_center/models.py b/homeassistant/components/resolution_center/models.py index eabfd98cef3..12e2f4e73f3 100644 --- a/homeassistant/components/resolution_center/models.py +++ b/homeassistant/components/resolution_center/models.py @@ -1,7 +1,11 @@ """Models for Resolution Center.""" from __future__ import annotations +from typing import Protocol + +from homeassistant import data_entry_flow from homeassistant.backports.enum import StrEnum +from homeassistant.core import HomeAssistant class IssueSeverity(StrEnum): @@ -10,3 +14,16 @@ class IssueSeverity(StrEnum): CRITICAL = "critical" ERROR = "error" WARNING = "warning" + + +class ResolutionCenterFlow(data_entry_flow.FlowHandler): + """Handle a flow for fixing an issue.""" + + +class ResolutionCenterProtocol(Protocol): + """Define the format of resolution center platforms.""" + + async def async_create_fix_flow( + self, hass: HomeAssistant, issue_id: str + ) -> ResolutionCenterFlow: + """Create a flow to fix a fixable issue.""" diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py index 14793f0bd2d..dfa4f1903d9 100644 --- a/homeassistant/components/resolution_center/websocket_api.py +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -2,13 +2,24 @@ from __future__ import annotations import dataclasses +from http import HTTPStatus from typing import Any +from aiohttp import web import voluptuous as vol +from homeassistant import data_entry_flow +from homeassistant.auth.permissions.const import POLICY_EDIT from homeassistant.components import websocket_api +from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import Unauthorized +from homeassistant.helpers.data_entry_flow import ( + FlowManagerIndexView, + FlowManagerResourceView, +) +from .const import DOMAIN from .issue_handler import async_dismiss_issue from .issue_registry import async_get as async_get_issue_registry @@ -19,6 +30,13 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_dismiss_issue) websocket_api.async_register_command(hass, ws_list_issues) + hass.http.register_view( + ResolutionCenterFlowIndexView(hass.data[DOMAIN]["flow_manager"]) + ) + hass.http.register_view( + ResolutionCenterFlowResourceView(hass.data[DOMAIN]["flow_manager"]) + ) + @callback @websocket_api.websocket_command( @@ -60,3 +78,63 @@ def ws_list_issues( ] connection.send_result(msg["id"], {"issues": issues}) + + +class ResolutionCenterFlowIndexView(FlowManagerIndexView): + """View to create issue fix flows.""" + + url = "/api/resolution_center/issues/fix" + name = "api:resolution_center:issues:fix" + + @RequestDataValidator( + vol.Schema( + { + vol.Required("handler"): str, + vol.Required("issue_id"): str, + }, + extra=vol.ALLOW_EXTRA, + ) + ) + async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: + """Handle a POST request.""" + if not request["hass_user"].is_admin: + raise Unauthorized(permission=POLICY_EDIT) + + try: + result = await self._flow_mgr.async_init( + data["handler"], + data={"issue_id": data["issue_id"]}, + ) + except data_entry_flow.UnknownHandler: + return self.json_message("Invalid handler specified", HTTPStatus.NOT_FOUND) + except data_entry_flow.UnknownStep: + return self.json_message( + "Handler does not support user", HTTPStatus.BAD_REQUEST + ) + + result = self._prepare_result_json(result) + + return self.json(result) # pylint: disable=arguments-differ + + +class ResolutionCenterFlowResourceView(FlowManagerResourceView): + """View to interact with the option flow manager.""" + + url = "/api/resolution_center/issues/fix/{flow_id}" + name = "api:resolution_center:issues:fix:resource" + + async def get(self, request: web.Request, flow_id: str) -> web.Response: + """Get the current state of a data_entry_flow.""" + if not request["hass_user"].is_admin: + raise Unauthorized(permission=POLICY_EDIT) + + return await super().get(request, flow_id) + + # pylint: disable=arguments-differ + async def post(self, request: web.Request, flow_id: str) -> web.Response: + """Handle a POST request.""" + if not request["hass_user"].is_admin: + raise Unauthorized(permission=POLICY_EDIT) + + # pylint: disable=no-value-for-parameter + return await super().post(request, flow_id) # type: ignore[no-any-return] diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py index 869c5d6c485..66f9bc42935 100644 --- a/tests/components/resolution_center/test_init.py +++ b/tests/components/resolution_center/test_init.py @@ -1,4 +1,6 @@ """Test the resolution center websocket API.""" +from unittest.mock import AsyncMock, Mock + import pytest from homeassistant.components.resolution_center import ( @@ -6,11 +8,16 @@ from homeassistant.components.resolution_center import ( async_delete_issue, ) from homeassistant.components.resolution_center.const import DOMAIN -from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.components.resolution_center.issue_handler import ( + async_dismiss_issue, + async_process_resolution_center_platforms, +) from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import mock_platform + async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: """Test creating and updating issues.""" @@ -29,6 +36,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: "breaks_in_ha_version": "2022.9.0dev0", "domain": "test", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -38,6 +46,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: "breaks_in_ha_version": "2022.8", "domain": "test", "issue_id": "issue_2", + "is_fixable": False, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", @@ -51,6 +60,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -78,6 +88,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["domain"], issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + is_fixable=issues[0]["is_fixable"], learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -109,6 +120,7 @@ async def test_create_issue_invalid_version( "breaks_in_ha_version": ha_version, "domain": "test", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -121,6 +133,7 @@ async def test_create_issue_invalid_version( issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -150,6 +163,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: { "breaks_in_ha_version": "2022.9", "domain": "test", + "is_fixable": True, "issue_id": "issue_1", "learn_more_url": "https://theuselessweb.com", "severity": "error", @@ -164,6 +178,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -246,6 +261,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["domain"], issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + is_fixable=issues[0]["is_fixable"], learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -275,6 +291,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: "breaks_in_ha_version": "2022.9", "domain": "fake_integration", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -288,6 +305,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -344,3 +362,25 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"] == {"issues": []} + + +async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> None: + """Test non-compliant platforms are not registered.""" + + hass.config.components.add("fake_integration") + hass.config.components.add("integration_without_diagnostics") + mock_platform( + hass, + "fake_integration.resolution_center", + Mock(async_create_fix_flow=AsyncMock(return_value=True)), + ) + mock_platform( + hass, + "integration_without_diagnostics.resolution_center", + Mock(spec=[]), + ) + assert await async_setup_component(hass, DOMAIN, {}) + + await async_process_resolution_center_platforms(hass) + + assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"] diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py index c236e96adb2..a0dffaacc8f 100644 --- a/tests/components/resolution_center/test_issue_registry.py +++ b/tests/components/resolution_center/test_issue_registry.py @@ -20,6 +20,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "breaks_in_ha_version": "2022.9", "domain": "test", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -29,6 +30,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "breaks_in_ha_version": "2022.8", "domain": "test", "issue_id": "issue_2", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", @@ -42,6 +44,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py index 9258a06f904..42899065121 100644 --- a/tests/components/resolution_center/test_websocket_api.py +++ b/tests/components/resolution_center/test_websocket_api.py @@ -1,22 +1,33 @@ """Test the resolution center websocket API.""" -from homeassistant.components.resolution_center import async_create_issue +from __future__ import annotations + +from http import HTTPStatus +from unittest.mock import ANY, AsyncMock, Mock + +import pytest +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.resolution_center import ( + ResolutionCenterFlow, + async_create_issue, +) from homeassistant.components.resolution_center.const import DOMAIN from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import mock_platform -async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: - """Test we can dismiss an issue.""" - assert await async_setup_component(hass, DOMAIN, {}) - - client = await hass_ws_client(hass) +async def create_issues(hass, ws_client): + """Create issues.""" issues = [ { "breaks_in_ha_version": "2022.9", - "domain": "test", + "domain": "fake_integration", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -30,14 +41,15 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) - msg = await client.receive_json() + await ws_client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await ws_client.receive_json() assert msg["success"] assert msg["result"] == { @@ -51,11 +63,63 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: ] } + return issues + + +class MockFixFlow(ResolutionCenterFlow): + """Handler for an issue fixing flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await (self.async_step_confirm()) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + return self.async_create_entry(title=None, data=None) + + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +@pytest.fixture(autouse=True) +async def mock_resolution_center_integration(hass): + """Mock a resolution_center integration.""" + hass.config.components.add("fake_integration") + hass.config.components.add("integration_without_diagnostics") + + def async_create_fix_flow(hass, issue_id): + return MockFixFlow() + + mock_platform( + hass, + "fake_integration.resolution_center", + Mock(async_create_fix_flow=AsyncMock(wraps=async_create_fix_flow)), + ) + mock_platform( + hass, + "integration_without_diagnostics.resolution_center", + Mock(spec=[]), + ) + + +async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can dismiss an issue.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issues = await create_issues(hass, client) + await client.send_json( { "id": 2, "type": "resolution_center/dismiss_issue", - "domain": "test", + "domain": "fake_integration", "issue_id": "no_such_issue", } ) @@ -66,7 +130,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: { "id": 3, "type": "resolution_center/dismiss_issue", - "domain": "test", + "domain": "fake_integration", "issue_id": "issue_1", } ) @@ -90,6 +154,185 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: } +async def test_fix_non_existing_issue( + hass: HomeAssistant, hass_client, hass_ws_client +) -> None: + """Test trying to fix an issue that doesn't exist.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + issues = await create_issues(hass, ws_client) + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "no_such_integration", "issue_id": "no_such_issue"} + ) + + assert resp.status != HTTPStatus.OK + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "no_such_issue"} + ) + + assert resp.status != HTTPStatus.OK + + await ws_client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + +async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> None: + """Test we can fix an issue.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues(hass, ws_client) + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "data_schema": [], + "description_placeholders": None, + "errors": None, + "flow_id": ANY, + "handler": "fake_integration", + "last_step": None, + "step_id": "confirm", + "type": "form", + } + + url = f"/api/resolution_center/issues/fix/{flow_id}" + # Test we can get the status of the flow + resp2 = await client.get(url) + + assert resp2.status == HTTPStatus.OK + data2 = await resp2.json() + + assert data == data2 + + resp = await client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "description": None, + "description_placeholders": None, + "flow_id": flow_id, + "handler": "fake_integration", + "title": None, + "type": "create_entry", + "version": 1, + } + + await ws_client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_fix_issue_unauth( + hass: HomeAssistant, hass_client, hass_admin_user +) -> None: + """Test we can't query the result if not authorized.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + hass_admin_user.groups = [] + + client = await hass_client() + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + + assert resp.status == HTTPStatus.UNAUTHORIZED + + +async def test_get_progress_unauth( + hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user +) -> None: + """Test we can't fix an issue if not authorized.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues(hass, ws_client) + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + flow_id = data["flow_id"] + + hass_admin_user.groups = [] + + url = f"/api/resolution_center/issues/fix/{flow_id}" + # Test we can't get the status of the flow + resp = await client.get(url) + assert resp.status == HTTPStatus.UNAUTHORIZED + + +async def test_step_unauth( + hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user +) -> None: + """Test we can't fix an issue if not authorized.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues(hass, ws_client) + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + flow_id = data["flow_id"] + + hass_admin_user.groups = [] + + url = f"/api/resolution_center/issues/fix/{flow_id}" + # Test we can't get the status of the flow + resp = await client.post(url) + assert resp.status == HTTPStatus.UNAUTHORIZED + + async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: """Test we can list issues.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -106,6 +349,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: { "breaks_in_ha_version": "2022.9", "domain": "test", + "is_fixable": True, "issue_id": "issue_1", "learn_more_url": "https://theuselessweb.com", "severity": "error", @@ -115,6 +359,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: { "breaks_in_ha_version": "2022.8", "domain": "test", + "is_fixable": False, "issue_id": "issue_2", "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", @@ -129,6 +374,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], From 397f94ee5023184f977a86bc8537bbe126b963b2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Jul 2022 20:06:13 +0200 Subject: [PATCH 403/820] Migrate DSMR to use keys for entity unique ID (#74895) --- homeassistant/components/dsmr/__init__.py | 91 ++++++++++++++- homeassistant/components/dsmr/sensor.py | 128 ++++++++++++++------- tests/components/dsmr/test_init.py | 133 ++++++++++++++++++++++ tests/components/dsmr/test_sensor.py | 6 +- 4 files changed, 311 insertions(+), 47 deletions(-) create mode 100644 tests/components/dsmr/test_init.py diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 044cfd14e64..f3546fc0a00 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -1,15 +1,29 @@ """The dsmr component.""" +from __future__ import annotations + from asyncio import CancelledError from contextlib import suppress +from typing import Any from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er -from .const import DATA_TASK, DOMAIN, PLATFORMS +from .const import CONF_DSMR_VERSION, DATA_TASK, DOMAIN, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DSMR from a config entry.""" + + @callback + def _async_migrate_entity_entry( + entity_entry: er.RegistryEntry, + ) -> dict[str, Any] | None: + """Migrate DSMR entity entry.""" + return async_migrate_entity_entry(entry, entity_entry) + + await er.async_migrate_entries(hass, entry.entry_id, _async_migrate_entity_entry) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} @@ -38,3 +52,76 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) + + +@callback +def async_migrate_entity_entry( + config_entry: ConfigEntry, entity_entry: er.RegistryEntry +) -> dict[str, Any] | None: + """Migrate DSMR entity entries. + + - Migrates unique ID for sensors based on entity description name to key. + """ + + # Replace names with keys in unique ID + for old, new in ( + ("Power_Consumption", "current_electricity_usage"), + ("Power_Production", "current_electricity_delivery"), + ("Power_Tariff", "electricity_active_tariff"), + ("Energy_Consumption_(tarif_1)", "electricity_used_tariff_1"), + ("Energy_Consumption_(tarif_2)", "electricity_used_tariff_2"), + ("Energy_Production_(tarif_1)", "electricity_delivered_tariff_1"), + ("Energy_Production_(tarif_2)", "electricity_delivered_tariff_2"), + ("Power_Consumption_Phase_L1", "instantaneous_active_power_l1_positive"), + ("Power_Consumption_Phase_L3", "instantaneous_active_power_l3_positive"), + ("Power_Consumption_Phase_L2", "instantaneous_active_power_l2_positive"), + ("Power_Production_Phase_L1", "instantaneous_active_power_l1_negative"), + ("Power_Production_Phase_L2", "instantaneous_active_power_l2_negative"), + ("Power_Production_Phase_L3", "instantaneous_active_power_l3_negative"), + ("Short_Power_Failure_Count", "short_power_failure_count"), + ("Long_Power_Failure_Count", "long_power_failure_count"), + ("Voltage_Sags_Phase_L1", "voltage_sag_l1_count"), + ("Voltage_Sags_Phase_L2", "voltage_sag_l2_count"), + ("Voltage_Sags_Phase_L3", "voltage_sag_l3_count"), + ("Voltage_Swells_Phase_L1", "voltage_swell_l1_count"), + ("Voltage_Swells_Phase_L2", "voltage_swell_l2_count"), + ("Voltage_Swells_Phase_L3", "voltage_swell_l3_count"), + ("Voltage_Phase_L1", "instantaneous_voltage_l1"), + ("Voltage_Phase_L2", "instantaneous_voltage_l2"), + ("Voltage_Phase_L3", "instantaneous_voltage_l3"), + ("Current_Phase_L1", "instantaneous_current_l1"), + ("Current_Phase_L2", "instantaneous_current_l2"), + ("Current_Phase_L3", "instantaneous_current_l3"), + ("Max_power_per_phase", "belgium_max_power_per_phase"), + ("Max_current_per_phase", "belgium_max_current_per_phase"), + ("Energy_Consumption_(total)", "electricity_imported_total"), + ("Energy_Production_(total)", "electricity_exported_total"), + ): + if entity_entry.unique_id.endswith(old): + return {"new_unique_id": entity_entry.unique_id.replace(old, new)} + + # Replace unique ID for gas sensors, based on DSMR version + old = "Gas_Consumption" + if entity_entry.unique_id.endswith(old): + dsmr_version = config_entry.data[CONF_DSMR_VERSION] + if dsmr_version in {"4", "5", "5L"}: + return { + "new_unique_id": entity_entry.unique_id.replace( + old, "hourly_gas_meter_reading" + ) + } + if dsmr_version == "5B": + return { + "new_unique_id": entity_entry.unique_id.replace( + old, "belgium_5min_gas_meter_reading" + ) + } + if dsmr_version == "2.2": + return { + "new_unique_id": entity_entry.unique_id.replace( + old, "gas_meter_reading" + ) + } + + # No migration needed + return None diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 1337f209d5d..6acb652c6e5 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -59,7 +59,16 @@ UNIT_CONVERSION = {"m3": VOLUME_CUBIC_METERS} @dataclass -class DSMRSensorEntityDescription(SensorEntityDescription): +class DSMRSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + obis_reference: str + + +@dataclass +class DSMRSensorEntityDescription( + SensorEntityDescription, DSMRSensorEntityDescriptionMixin +): """Represents an DSMR Sensor.""" dsmr_versions: set[str] | None = None @@ -68,211 +77,239 @@ class DSMRSensorEntityDescription(SensorEntityDescription): SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_USAGE, + key="current_electricity_usage", name="Power Consumption", + obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE, device_class=SensorDeviceClass.POWER, force_update=True, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_DELIVERY, + key="electricity_delivery", name="Power Production", + obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, device_class=SensorDeviceClass.POWER, force_update=True, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_ACTIVE_TARIFF, + key="electricity_active_tariff", name="Power Tariff", + obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, icon="mdi:flash", ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_1, + key="electricity_used_tariff_1", name="Energy Consumption (tarif 1)", + obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=SensorDeviceClass.ENERGY, force_update=True, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_2, + key="electricity_used_tariff_2", name="Energy Consumption (tarif 2)", + obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, + key="electricity_delivered_tariff_1", name="Energy Production (tarif 1)", + obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, + key="electricity_delivered_tariff_2", name="Energy Production (tarif 2)", + obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + key="instantaneous_active_power_l1_positive", name="Power Consumption Phase L1", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + key="instantaneous_active_power_l2_positive", name="Power Consumption Phase L2", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + key="instantaneous_active_power_l3_positive", name="Power Consumption Phase L3", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + key="instantaneous_active_power_l1_negative", name="Power Production Phase L1", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + key="instantaneous_active_power_l2_negative", name="Power Production Phase L2", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + key="instantaneous_active_power_l3_negative", name="Power Production Phase L3", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.SHORT_POWER_FAILURE_COUNT, + key="short_power_failure_count", name="Short Power Failure Count", + obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.LONG_POWER_FAILURE_COUNT, + key="long_power_failure_count", name="Long Power Failure Count", + obis_reference=obis_references.LONG_POWER_FAILURE_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L1_COUNT, + key="voltage_sag_l1_count", name="Voltage Sags Phase L1", + obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L2_COUNT, + key="voltage_sag_l2_count", name="Voltage Sags Phase L2", + obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L3_COUNT, + key="voltage_sag_l3_count", name="Voltage Sags Phase L3", + obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L1_COUNT, + key="voltage_swell_l1_count", name="Voltage Swells Phase L1", + obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L2_COUNT, + key="voltage_swell_l2_count", name="Voltage Swells Phase L2", + obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L3_COUNT, + key="voltage_swell_l3_count", name="Voltage Swells Phase L3", + obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L1, + key="instantaneous_voltage_l1", name="Voltage Phase L1", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L2, + key="instantaneous_voltage_l2", name="Voltage Phase L2", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L3, + key="instantaneous_voltage_l3", name="Voltage Phase L3", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L1, + key="instantaneous_current_l1", name="Current Phase L1", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L2, + key="instantaneous_current_l2", name="Current Phase L2", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L3, + key="instantaneous_current_l3", name="Current Phase L3", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_POWER_PER_PHASE, + key="belgium_max_power_per_phase", name="Max power per phase", + obis_reference=obis_references.BELGIUM_MAX_POWER_PER_PHASE, dsmr_versions={"5B"}, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -280,8 +317,9 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, + key="belgium_max_current_per_phase", name="Max current per phase", + obis_reference=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, dsmr_versions={"5B"}, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -289,24 +327,27 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_IMPORTED_TOTAL, + key="electricity_imported_total", name="Energy Consumption (total)", + obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL, dsmr_versions={"5L", "5S", "Q3D"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_EXPORTED_TOTAL, + key="electricity_exported_total", name="Energy Production (total)", + obis_reference=obis_references.ELECTRICITY_EXPORTED_TOTAL, dsmr_versions={"5L", "5S", "Q3D"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.HOURLY_GAS_METER_READING, + key="hourly_gas_meter_reading", name="Gas Consumption", + obis_reference=obis_references.HOURLY_GAS_METER_READING, dsmr_versions={"4", "5", "5L"}, is_gas=True, force_update=True, @@ -314,8 +355,9 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.BELGIUM_5MIN_GAS_METER_READING, + key="belgium_5min_gas_meter_reading", name="Gas Consumption", + obis_reference=obis_references.BELGIUM_5MIN_GAS_METER_READING, dsmr_versions={"5B"}, is_gas=True, force_update=True, @@ -323,8 +365,9 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.GAS_METER_READING, + key="gas_meter_reading", name="Gas Consumption", + obis_reference=obis_references.GAS_METER_READING, dsmr_versions={"2.2"}, is_gas=True, force_update=True, @@ -492,25 +535,23 @@ class DSMREntity(SensorEntity): identifiers={(DOMAIN, device_serial)}, name=device_name, ) - self._attr_unique_id = f"{device_serial}_{entity_description.name}".replace( - " ", "_" - ) + self._attr_unique_id = f"{device_serial}_{entity_description.key}" @callback def update_data(self, telegram: dict[str, DSMRObject]) -> None: """Update data.""" self.telegram = telegram - if self.hass and self.entity_description.key in self.telegram: + if self.hass and self.entity_description.obis_reference in self.telegram: self.async_write_ha_state() def get_dsmr_object_attr(self, attribute: str) -> str | None: """Read attribute from last received telegram for this DSMR object.""" # Make sure telegram contains an object for this entities obis - if self.entity_description.key not in self.telegram: + if self.entity_description.obis_reference not in self.telegram: return None # Get the attribute value if the object has it - dsmr_object = self.telegram[self.entity_description.key] + dsmr_object = self.telegram[self.entity_description.obis_reference] attr: str | None = getattr(dsmr_object, attribute) return attr @@ -520,7 +561,10 @@ class DSMREntity(SensorEntity): if (value := self.get_dsmr_object_attr("value")) is None: return None - if self.entity_description.key == obis_references.ELECTRICITY_ACTIVE_TARIFF: + if ( + self.entity_description.obis_reference + == obis_references.ELECTRICITY_ACTIVE_TARIFF + ): return self.translate_tariff(value, self._entry.data[CONF_DSMR_VERSION]) with suppress(TypeError): diff --git a/tests/components/dsmr/test_init.py b/tests/components/dsmr/test_init.py new file mode 100644 index 00000000000..914a9f6bdaf --- /dev/null +++ b/tests/components/dsmr/test_init.py @@ -0,0 +1,133 @@ +"""Tests for the DSMR integration.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.dsmr.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "dsmr_version,old_unique_id,new_unique_id", + [ + ("5", "1234_Power_Consumption", "1234_current_electricity_usage"), + ("5", "1234_Power_Production", "1234_current_electricity_delivery"), + ("5", "1234_Power_Tariff", "1234_electricity_active_tariff"), + ("5", "1234_Energy_Consumption_(tarif_1)", "1234_electricity_used_tariff_1"), + ("5", "1234_Energy_Consumption_(tarif_2)", "1234_electricity_used_tariff_2"), + ( + "5", + "1234_Energy_Production_(tarif_1)", + "1234_electricity_delivered_tariff_1", + ), + ( + "5", + "1234_Energy_Production_(tarif_2)", + "1234_electricity_delivered_tariff_2", + ), + ( + "5", + "1234_Power_Consumption_Phase_L1", + "1234_instantaneous_active_power_l1_positive", + ), + ( + "5", + "1234_Power_Consumption_Phase_L2", + "1234_instantaneous_active_power_l2_positive", + ), + ( + "5", + "1234_Power_Consumption_Phase_L3", + "1234_instantaneous_active_power_l3_positive", + ), + ( + "5", + "1234_Power_Production_Phase_L1", + "1234_instantaneous_active_power_l1_negative", + ), + ( + "5", + "1234_Power_Production_Phase_L2", + "1234_instantaneous_active_power_l2_negative", + ), + ( + "5", + "1234_Power_Production_Phase_L3", + "1234_instantaneous_active_power_l3_negative", + ), + ("5", "1234_Short_Power_Failure_Count", "1234_short_power_failure_count"), + ("5", "1234_Long_Power_Failure_Count", "1234_long_power_failure_count"), + ("5", "1234_Voltage_Sags_Phase_L1", "1234_voltage_sag_l1_count"), + ("5", "1234_Voltage_Sags_Phase_L2", "1234_voltage_sag_l2_count"), + ("5", "1234_Voltage_Sags_Phase_L3", "1234_voltage_sag_l3_count"), + ("5", "1234_Voltage_Swells_Phase_L1", "1234_voltage_swell_l1_count"), + ("5", "1234_Voltage_Swells_Phase_L2", "1234_voltage_swell_l2_count"), + ("5", "1234_Voltage_Swells_Phase_L3", "1234_voltage_swell_l3_count"), + ("5", "1234_Voltage_Phase_L1", "1234_instantaneous_voltage_l1"), + ("5", "1234_Voltage_Phase_L2", "1234_instantaneous_voltage_l2"), + ("5", "1234_Voltage_Phase_L3", "1234_instantaneous_voltage_l3"), + ("5", "1234_Current_Phase_L1", "1234_instantaneous_current_l1"), + ("5", "1234_Current_Phase_L2", "1234_instantaneous_current_l2"), + ("5", "1234_Current_Phase_L3", "1234_instantaneous_current_l3"), + ("5B", "1234_Max_power_per_phase", "1234_belgium_max_power_per_phase"), + ("5B", "1234_Max_current_per_phase", "1234_belgium_max_current_per_phase"), + ("5L", "1234_Energy_Consumption_(total)", "1234_electricity_imported_total"), + ("5L", "1234_Energy_Production_(total)", "1234_electricity_exported_total"), + ("5L", "1234_Energy_Production_(total)", "1234_electricity_exported_total"), + ("5", "1234_Gas_Consumption", "1234_hourly_gas_meter_reading"), + ("5B", "1234_Gas_Consumption", "1234_belgium_5min_gas_meter_reading"), + ("2.2", "1234_Gas_Consumption", "1234_gas_meter_reading"), + ], +) +async def test_migrate_unique_id( + hass: HomeAssistant, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], + dsmr_version: str, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test migration of unique_id.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": dsmr_version, + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="my_sensor", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) + is None + ) + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, new_unique_id) + == "sensor.my_sensor" + ) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 4502f61586a..b006765bde7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -79,11 +79,11 @@ async def test_default_setup(hass, dsmr_connection_fixture): entry = registry.async_get("sensor.power_consumption") assert entry - assert entry.unique_id == "1234_Power_Consumption" + assert entry.unique_id == "1234_current_electricity_usage" entry = registry.async_get("sensor.gas_consumption") assert entry - assert entry.unique_id == "5678_Gas_Consumption" + assert entry.unique_id == "5678_gas_meter_reading" telegram_callback = connection_factory.call_args_list[0][0][2] @@ -157,7 +157,7 @@ async def test_setup_only_energy(hass, dsmr_connection_fixture): entry = registry.async_get("sensor.power_consumption") assert entry - assert entry.unique_id == "1234_Power_Consumption" + assert entry.unique_id == "1234_current_electricity_usage" entry = registry.async_get("sensor.gas_consumption") assert not entry From 96ecbe4388e5baac497b5cf7f4378b6360c0a1c6 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Tue, 12 Jul 2022 14:45:38 -0400 Subject: [PATCH 404/820] Migrate Environment Canada to new entity naming style (#75024) Co-authored-by: Franck Nijhof --- .../components/environment_canada/__init__.py | 13 +++++++ .../components/environment_canada/camera.py | 6 ++- .../components/environment_canada/sensor.py | 38 ++++++++++--------- .../components/environment_canada/weather.py | 7 ++-- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index e12d12b87d0..a8548429d50 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -9,6 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN @@ -87,6 +89,17 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +def device_info(config_entry: ConfigEntry) -> DeviceInfo: + """Build and return the device info for EC.""" + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer="Environment Canada", + name=config_entry.title, + configuration_url="https://weather.gc.ca/", + ) + + class ECDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching EC data.""" diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index e415bab977b..7b93f0b28f4 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -12,6 +12,7 @@ from homeassistant.helpers.entity_platform import ( ) from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import device_info from .const import ATTR_OBSERVATION_TIME, DOMAIN SERVICE_SET_RADAR_TYPE = "set_radar_type" @@ -40,16 +41,19 @@ async def async_setup_entry( class ECCamera(CoordinatorEntity, Camera): """Implementation of an Environment Canada radar camera.""" + _attr_has_entity_name = True + _attr_name = "Radar" + def __init__(self, coordinator): """Initialize the camera.""" super().__init__(coordinator) Camera.__init__(self) self.radar_object = coordinator.ec_data - self._attr_name = f"{coordinator.config_entry.title} Radar" self._attr_unique_id = f"{coordinator.config_entry.unique_id}-radar" self._attr_attribution = self.radar_object.metadata["attribution"] self._attr_entity_registry_enabled_default = False + self._attr_device_info = device_info(coordinator.config_entry) self.content_type = "image/gif" diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 2e124d1ec7c..08da60fe01f 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -27,6 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import device_info from .const import ATTR_STATION, DOMAIN ATTR_TIME = "alert time" @@ -51,12 +52,12 @@ class ECSensorEntityDescription( SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ECSensorEntityDescription( key="condition", - name="Current Condition", + name="Current condition", value_fn=lambda data: data.conditions.get("condition", {}).get("value"), ), ECSensorEntityDescription( key="dewpoint", - name="Dew Point", + name="Dew point", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -64,7 +65,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="high_temp", - name="High Temperature", + name="High temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -88,12 +89,12 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="icon_code", - name="Icon Code", + name="Icon code", value_fn=lambda data: data.conditions.get("icon_code", {}).get("value"), ), ECSensorEntityDescription( key="low_temp", - name="Low Temperature", + name="Low temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -101,34 +102,34 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="normal_high", - name="Normal High Temperature", + name="Normal high temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, value_fn=lambda data: data.conditions.get("normal_high", {}).get("value"), ), ECSensorEntityDescription( key="normal_low", - name="Normal Low Temperature", + name="Normal low temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, value_fn=lambda data: data.conditions.get("normal_low", {}).get("value"), ), ECSensorEntityDescription( key="pop", - name="Chance of Precipitation", + name="Chance of precipitation", native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: data.conditions.get("pop", {}).get("value"), ), ECSensorEntityDescription( key="precip_yesterday", - name="Precipitation Yesterday", + name="Precipitation yesterday", native_unit_of_measurement=LENGTH_MILLIMETERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"), ), ECSensorEntityDescription( key="pressure", - name="Barometric Pressure", + name="Barometric pressure", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_KPA, state_class=SensorStateClass.MEASUREMENT, @@ -156,13 +157,13 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="timestamp", - name="Observation Time", + name="Observation time", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.metadata.get("timestamp"), ), ECSensorEntityDescription( key="uv_index", - name="UV Index", + name="UV index", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("uv_index", {}).get("value"), @@ -176,13 +177,13 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="wind_bearing", - name="Wind Bearing", + name="Wind bearing", native_unit_of_measurement=DEGREE, value_fn=lambda data: data.conditions.get("wind_bearing", {}).get("value"), ), ECSensorEntityDescription( key="wind_chill", - name="Wind Chill", + name="Wind chill", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -190,19 +191,19 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="wind_dir", - name="Wind Direction", + name="Wind direction", value_fn=lambda data: data.conditions.get("wind_dir", {}).get("value"), ), ECSensorEntityDescription( key="wind_gust", - name="Wind Gust", + name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_gust", {}).get("value"), ), ECSensorEntityDescription( key="wind_speed", - name="Wind Speed", + name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_speed", {}).get("value"), @@ -285,6 +286,7 @@ class ECBaseSensor(CoordinatorEntity, SensorEntity): """Environment Canada sensor base.""" entity_description: ECSensorEntityDescription + _attr_has_entity_name = True def __init__(self, coordinator, description): """Initialize the base sensor.""" @@ -292,8 +294,8 @@ class ECBaseSensor(CoordinatorEntity, SensorEntity): self.entity_description = description self._ec_data = coordinator.ec_data self._attr_attribution = self._ec_data.metadata["attribution"] - self._attr_name = f"{coordinator.config_entry.title} {description.name}" self._attr_unique_id = f"{coordinator.config_entry.title}-{description.key}" + self._attr_device_info = device_info(coordinator.config_entry) @property def native_value(self): diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 40706ffb6c1..8dbf8c15731 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -35,6 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt +from . import device_info from .const import DOMAIN # Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/ @@ -68,6 +69,7 @@ async def async_setup_entry( class ECWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_has_entity_name = True _attr_native_pressure_unit = PRESSURE_KPA _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_visibility_unit = LENGTH_KILOMETERS @@ -78,14 +80,13 @@ class ECWeather(CoordinatorEntity, WeatherEntity): super().__init__(coordinator) self.ec_data = coordinator.ec_data self._attr_attribution = self.ec_data.metadata["attribution"] - self._attr_name = ( - f"{coordinator.config_entry.title}{' Hourly' if hourly else ''}" - ) + self._attr_name = "Hourly forecast" if hourly else "Forecast" self._attr_unique_id = ( f"{coordinator.config_entry.unique_id}{'-hourly' if hourly else '-daily'}" ) self._attr_entity_registry_enabled_default = not hourly self._hourly = hourly + self._attr_device_info = device_info(coordinator.config_entry) @property def native_temperature(self): From d40978742cf3d095b9fd980abb5dcf39a8194b75 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Jul 2022 20:46:04 +0200 Subject: [PATCH 405/820] Update coverage to 6.4.2 (#75072) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 1ca1fc05819..adecd327631 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.4.1 +coverage==6.4.2 freezegun==1.2.1 mock-open==1.4.0 mypy==0.961 From b54fe14a10a7a0ee3762c217c32cc6582ace406c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 12 Jul 2022 12:53:21 -0600 Subject: [PATCH 406/820] Replace Guardian `reboot` and `reset_valve_diagnostics` services with buttons (#75028) --- .coveragerc | 1 + homeassistant/components/guardian/__init__.py | 96 +++++++++++---- homeassistant/components/guardian/button.py | 113 ++++++++++++++++++ 3 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/guardian/button.py diff --git a/.coveragerc b/.coveragerc index 4e252fb9c92..94b9eceb11b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -450,6 +450,7 @@ omit = homeassistant/components/gtfs/sensor.py homeassistant/components/guardian/__init__.py homeassistant/components/guardian/binary_sensor.py + homeassistant/components/guardian/button.py homeassistant/components/guardian/sensor.py homeassistant/components/guardian/switch.py homeassistant/components/guardian/util.py diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 3707bf9d2eb..f13ca1a7ff5 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -88,8 +88,7 @@ SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( }, ) - -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] @callback @@ -106,6 +105,25 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) raise ValueError(f"No client for device ID: {device_id}") +@callback +def async_log_deprecated_service_call( + hass: HomeAssistant, + call: ServiceCall, + alternate_service: str, + alternate_target: str, +) -> None: + """Log a warning about a deprecated service call.""" + LOGGER.warning( + ( + 'The "%s" service is deprecated and will be removed in a future version; ' + 'use the "%s" service and pass it a target entity ID of "%s"' + ), + f"{call.domain}.{call.service}", + alternate_service, + alternate_target, + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) @@ -164,17 +182,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback - def extract_client(func: Callable) -> Callable: - """Define a decorator to get the correct client for a service call.""" + def hydrate_with_entry_and_client(func: Callable) -> Callable: + """Define a decorator to hydrate a method with args based on service call.""" async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" entry_id = async_get_entry_id_for_service_call(hass, call) client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + entry = hass.config_entries.async_get_entry(entry_id) + assert entry try: async with client: - await func(call, client) + await func(call, entry, client) except GuardianError as err: raise HomeAssistantError( f"Error while executing {func.__name__}: {err}" @@ -182,48 +202,76 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper - @extract_client - async def async_disable_ap(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_disable_ap( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Disable the onboard AP.""" await client.wifi.disable_ap() - @extract_client - async def async_enable_ap(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_enable_ap( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Enable the onboard AP.""" await client.wifi.enable_ap() - @extract_client - async def async_pair_sensor(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_pair_sensor( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Add a new paired sensor.""" - entry_id = async_get_entry_id_for_service_call(hass, call) - paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] + paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER + ] uid = call.data[CONF_UID] await client.sensor.pair_sensor(uid) await paired_sensor_manager.async_pair_sensor(uid) - @extract_client - async def async_reboot(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_reboot( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Reboot the valve controller.""" + async_log_deprecated_service_call( + hass, + call, + "button.press", + f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reboot", + ) await client.system.reboot() - @extract_client - async def async_reset_valve_diagnostics(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_reset_valve_diagnostics( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Fully reset system motor diagnostics.""" + async_log_deprecated_service_call( + hass, + call, + "button.press", + f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reset_valve_diagnostics", + ) await client.valve.reset() - @extract_client - async def async_unpair_sensor(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_unpair_sensor( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Remove a paired sensor.""" - entry_id = async_get_entry_id_for_service_call(hass, call) - paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] + paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER + ] uid = call.data[CONF_UID] await client.sensor.unpair_sensor(uid) await paired_sensor_manager.async_unpair_sensor(uid) - @extract_client - async def async_upgrade_firmware(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_upgrade_firmware( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Upgrade the device firmware.""" await client.system.upgrade_firmware( url=call.data[CONF_URL], @@ -389,7 +437,6 @@ class GuardianEntity(CoordinatorEntity): This should be extended by Guardian platforms. """ - raise NotImplementedError class PairedSensorEntity(GuardianEntity): @@ -454,7 +501,6 @@ class ValveControllerEntity(GuardianEntity): This should be extended by Guardian platforms. """ - raise NotImplementedError @callback def async_add_coordinator_update_listener(self, api: str) -> None: diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py new file mode 100644 index 00000000000..e7cc757d367 --- /dev/null +++ b/homeassistant/components/guardian/button.py @@ -0,0 +1,113 @@ +"""Buttons for the Elexa Guardian integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from aioguardian import Client +from aioguardian.errors import GuardianError + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import ValveControllerEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +@dataclass +class GuardianButtonDescriptionMixin: + """Define an entity description mixin for Guardian buttons.""" + + push_action: Callable[[Client], Awaitable] + + +@dataclass +class GuardianButtonDescription( + ButtonEntityDescription, GuardianButtonDescriptionMixin +): + """Describe a Guardian button description.""" + + +BUTTON_KIND_REBOOT = "reboot" +BUTTON_KIND_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" + + +async def _async_reboot(client: Client) -> None: + """Reboot the Guardian.""" + await client.system.reboot() + + +async def _async_valve_reset(client: Client) -> None: + """Reset the valve diagnostics on the Guardian.""" + await client.valve.reset() + + +BUTTON_DESCRIPTIONS = ( + GuardianButtonDescription( + key=BUTTON_KIND_REBOOT, + name="Reboot", + push_action=_async_reboot, + ), + GuardianButtonDescription( + key=BUTTON_KIND_RESET_VALVE_DIAGNOSTICS, + name="Reset valve diagnostics", + push_action=_async_valve_reset, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Guardian buttons based on a config entry.""" + async_add_entities( + [ + GuardianButton( + entry, + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], + description, + ) + for description in BUTTON_DESCRIPTIONS + ] + ) + + +class GuardianButton(ValveControllerEntity, ButtonEntity): + """Define a Guardian button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG + + entity_description: GuardianButtonDescription + + def __init__( + self, + entry: ConfigEntry, + client: Client, + coordinators: dict[str, DataUpdateCoordinator], + description: GuardianButtonDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, coordinators, description) + + self._client = client + + async def async_press(self) -> None: + """Send out a restart command.""" + try: + async with self._client: + await self.entity_description.push_action(self._client) + except GuardianError as err: + raise HomeAssistantError( + f'Error while pressing button "{self.entity_id}": {err}' + ) from err From 41ec8cd3549cd649184409c0d4bf6c4eb149411b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jul 2022 13:49:54 -0700 Subject: [PATCH 407/820] Expose supported brands via API (#75074) --- .pre-commit-config.yaml | 2 +- .../components/websocket_api/commands.py | 24 ++++++++ homeassistant/generated/supported_brands.py | 15 +++++ script/hassfest/__main__.py | 2 + script/hassfest/model.py | 5 ++ script/hassfest/supported_brands.py | 55 +++++++++++++++++++ .../components/websocket_api/test_commands.py | 41 +++++++++++++- 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 homeassistant/generated/supported_brands.py create mode 100644 script/hassfest/supported_brands.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bbf7295be99..51429bdf94b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] - exclude: ^tests/fixtures/ + exclude: ^tests/fixtures/|homeassistant/generated/ - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 hooks: diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index bea08722eb0..b7e7a353633 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -22,6 +22,7 @@ from homeassistant.exceptions import ( TemplateError, Unauthorized, ) +from homeassistant.generated import supported_brands from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( @@ -68,6 +69,7 @@ def async_register_commands( async_reg(hass, handle_unsubscribe_events) async_reg(hass, handle_validate_config) async_reg(hass, handle_subscribe_entities) + async_reg(hass, handle_supported_brands) def pong_message(iden: int) -> dict[str, Any]: @@ -691,3 +693,25 @@ async def handle_validate_config( result[key] = {"valid": True, "error": None} connection.send_result(msg["id"], result) + + +@decorators.websocket_command( + { + vol.Required("type"): "supported_brands", + } +) +@decorators.async_response +async def handle_supported_brands( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle supported brands command.""" + data = {} + for integration in await asyncio.gather( + *[ + async_get_integration(hass, integration) + for integration in supported_brands.HAS_SUPPORTED_BRANDS + ] + ): + data[integration.domain] = integration.manifest["supported_brands"] + + connection.send_result(msg["id"], data) diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py new file mode 100644 index 00000000000..589e0462cf7 --- /dev/null +++ b/homeassistant/generated/supported_brands.py @@ -0,0 +1,15 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +""" + +# fmt: off + +HAS_SUPPORTED_BRANDS = ( + "denonavr", + "hunterdouglas_powerview", + "motion_blinds", + "overkiz", + "renault", + "wemo" +) diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 4bc30583d47..233abda4ed8 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -20,6 +20,7 @@ from . import ( requirements, services, ssdp, + supported_brands, translations, usb, zeroconf, @@ -39,6 +40,7 @@ INTEGRATION_PLUGINS = [ requirements, services, ssdp, + supported_brands, translations, usb, zeroconf, diff --git a/script/hassfest/model.py b/script/hassfest/model.py index fc38e1db592..d4e1fbf806a 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -112,6 +112,11 @@ class Integration: """List of dependencies.""" return self.manifest.get("dependencies", []) + @property + def supported_brands(self) -> dict[str]: + """Return dict of supported brands.""" + return self.manifest.get("supported_brands", {}) + @property def integration_type(self) -> str: """Get integration_type.""" diff --git a/script/hassfest/supported_brands.py b/script/hassfest/supported_brands.py new file mode 100644 index 00000000000..6740260a04c --- /dev/null +++ b/script/hassfest/supported_brands.py @@ -0,0 +1,55 @@ +"""Generate supported_brands data.""" +from __future__ import annotations + +import json + +from .model import Config, Integration + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +\"\"\" + +# fmt: off + +HAS_SUPPORTED_BRANDS = ({}) +""".strip() + + +def generate_and_validate(integrations: dict[str, Integration], config: Config) -> str: + """Validate and generate supported_brands data.""" + + brands = [ + domain + for domain, integration in sorted(integrations.items()) + if integration.supported_brands + ] + + return BASE.format(json.dumps(brands, indent=4)[1:-1]) + + +def validate(integrations: dict[str, Integration], config: Config) -> None: + """Validate supported_brands data.""" + supported_brands_path = config.root / "homeassistant/generated/supported_brands.py" + config.cache["supported_brands"] = content = generate_and_validate( + integrations, config + ) + + if config.specific_integrations: + return + + if supported_brands_path.read_text(encoding="utf-8").strip() != content: + config.add_error( + "supported_brands", + "File supported_brands.py is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + + +def generate(integrations: dict[str, Integration], config: Config): + """Generate supported_brands data.""" + supported_brands_path = config.root / "homeassistant/generated/supported_brands.py" + supported_brands_path.write_text( + f"{config.cache['supported_brands']}\n", encoding="utf-8" + ) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 0f4695596fc..f1065061c73 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -22,7 +22,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.loader import async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_setup_component -from tests.common import MockEntity, MockEntityPlatform, async_mock_service +from tests.common import ( + MockEntity, + MockEntityPlatform, + MockModule, + async_mock_service, + mock_integration, +) STATE_KEY_SHORT_NAMES = { "entity_id": "e", @@ -1749,3 +1755,36 @@ async def test_validate_config_invalid(websocket_client, key, config, error): assert msg["type"] == const.TYPE_RESULT assert msg["success"] assert msg["result"] == {key: {"valid": False, "error": error}} + + +async def test_supported_brands(hass, websocket_client): + """Test supported brands.""" + mock_integration( + hass, + MockModule("test", partial_manifest={"supported_brands": {"hello": "World"}}), + ) + mock_integration( + hass, + MockModule( + "abcd", partial_manifest={"supported_brands": {"something": "Something"}} + ), + ) + + with patch( + "homeassistant.generated.supported_brands.HAS_SUPPORTED_BRANDS", + ("abcd", "test"), + ): + await websocket_client.send_json({"id": 7, "type": "supported_brands"}) + msg = await websocket_client.receive_json() + + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == { + "abcd": { + "something": "Something", + }, + "test": { + "hello": "World", + }, + } From a3fd5acf3f189126ebe7eb8ad7241679184157fe Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Jul 2022 00:27:34 +0000 Subject: [PATCH 408/820] [ci skip] Translation update --- .../components/abode/translations/ja.json | 2 +- .../components/abode/translations/pt.json | 2 +- .../accuweather/translations/ja.json | 2 +- .../components/adax/translations/pt.json | 17 +++++++++++++ .../components/adguard/translations/pt.json | 5 +++- .../components/aemet/translations/pt.json | 7 ++++++ .../components/airthings/translations/pt.json | 7 ++++++ .../components/airvisual/translations/pt.json | 7 +++++- .../components/airzone/translations/pt.json | 12 +++++++++ .../aladdin_connect/translations/pt.json | 17 +++++++++++++ .../components/almond/translations/ja.json | 2 +- .../components/ambee/translations/pt.json | 6 +++++ .../ambient_station/translations/pt.json | 2 +- .../components/androidtv/translations/pt.json | 8 ++++++ .../components/anthemav/translations/pt.json | 18 +++++++++++++ .../aseko_pool_live/translations/pt.json | 12 +++++++++ .../components/asuswrt/translations/pt.json | 12 +++++++++ .../aurora_abb_powerone/translations/pt.json | 7 ++++++ .../aussie_broadband/translations/pt.json | 20 +++++++++++++++ .../components/awair/translations/pt.json | 6 +++++ .../azure_event_hub/translations/ja.json | 2 +- .../azure_event_hub/translations/pt.json | 7 ++++++ .../components/baf/translations/pt.json | 7 ++++++ .../components/balboa/translations/pt.json | 14 +++++++++++ .../binary_sensor/translations/pt.json | 5 +++- .../components/blink/translations/pt.json | 3 ++- .../components/bosch_shc/translations/pt.json | 17 +++++++++++++ .../components/braviatv/translations/pt.json | 2 +- .../components/brunt/translations/pt.json | 21 ++++++++++++++++ .../components/bsblan/translations/pt.json | 3 ++- .../buienradar/translations/pt.json | 11 ++++++++ .../components/canary/translations/ja.json | 2 +- .../components/cast/translations/ja.json | 2 +- .../components/cast/translations/pt.json | 2 +- .../cloudflare/translations/ja.json | 2 +- .../cloudflare/translations/pt.json | 1 + .../components/co2signal/translations/pt.json | 14 +++++++++++ .../components/coinbase/translations/pt.json | 8 ++++++ .../components/cpuspeed/translations/ja.json | 2 +- .../components/cpuspeed/translations/pt.json | 7 ++++++ .../crownstone/translations/pt.json | 16 ++++++++++++ .../components/deluge/translations/pt.json | 18 +++++++++++++ .../derivative/translations/pt.json | 9 +++++++ .../devolo_home_control/translations/pt.json | 5 ++++ .../dialogflow/translations/ja.json | 2 +- .../components/discord/translations/pt.json | 23 +++++++++++++++++ .../components/dlna_dmr/translations/pt.json | 14 +++++++++++ .../components/dlna_dms/translations/pt.json | 15 +++++++++++ .../components/dsmr/translations/pt.json | 16 ++++++++++++ .../components/ecobee/translations/ja.json | 2 +- .../components/econet/translations/pt.json | 10 +++++++- .../components/efergy/translations/pt.json | 10 ++++++++ .../eight_sleep/translations/pt.json | 15 +++++++++++ .../components/elkm1/translations/pt.json | 13 ++++++++++ .../components/elmax/translations/pt.json | 15 +++++++++++ .../components/emonitor/translations/pt.json | 14 +++++++++++ .../components/enocean/translations/ja.json | 2 +- .../enphase_envoy/translations/pt.json | 17 +++++++++++++ .../environment_canada/translations/pt.json | 12 +++++++++ .../components/esphome/translations/pt.json | 3 ++- .../evil_genius_labs/translations/pt.json | 5 +++- .../components/ezviz/translations/pt.json | 5 ++++ .../components/fibaro/translations/pt.json | 7 ++++++ .../components/firmata/translations/pt.json | 2 +- .../components/fivem/translations/pt.json | 15 +++++++++++ .../fjaraskupan/translations/ja.json | 2 +- .../components/flipr/translations/pt.json | 8 ++++++ .../components/flux_led/translations/pt.json | 15 +++++++++++ .../forecast_solar/translations/pt.json | 11 ++++++++ .../components/foscam/translations/pt.json | 5 ++++ .../components/fritz/translations/pt.json | 9 ++++++- .../fritzbox_callmonitor/translations/pt.json | 11 ++++++++ .../components/fronius/translations/pt.json | 14 +++++++++++ .../components/generic/translations/ja.json | 2 +- .../components/generic/translations/pt.json | 22 ++++++++++++++++ .../geocaching/translations/pt.json | 20 +++++++++++++++ .../components/geofency/translations/ja.json | 2 +- .../components/github/translations/pt.json | 7 ++++++ .../components/goodwe/translations/pt.json | 7 ++++++ .../components/google/translations/pt.json | 19 ++++++++++++++ .../google_travel_time/translations/pt.json | 11 ++++++++ .../components/gpslogger/translations/ja.json | 2 +- .../components/gree/translations/ja.json | 2 +- .../components/group/translations/pt.json | 3 +-- .../growatt_server/translations/pt.json | 12 +++++++++ .../components/hangouts/translations/pt.json | 2 +- .../components/heos/translations/ja.json | 2 +- .../hisense_aehw4a1/translations/ja.json | 2 +- .../home_plus_control/translations/ja.json | 2 +- .../components/homekit/translations/pt.json | 1 + .../homematicip_cloud/translations/pt.json | 2 +- .../components/honeywell/translations/pt.json | 11 ++++++++ .../huawei_lte/translations/pt.json | 2 +- .../huisbaasje/translations/pt.json | 11 +++++++- .../components/ialarm/translations/pt.json | 7 ++++++ .../components/iaqualink/translations/ja.json | 2 +- .../components/iaqualink/translations/pt.json | 2 +- .../components/ifttt/translations/ja.json | 2 +- .../components/insteon/translations/ja.json | 2 +- .../components/insteon/translations/pt.json | 2 +- .../intellifire/translations/pt.json | 16 ++++++++++++ .../components/ios/translations/ja.json | 2 +- .../components/ios/translations/pt.json | 2 +- .../components/iotawatt/translations/pt.json | 10 +++++++- .../islamic_prayer_times/translations/ja.json | 2 +- .../components/iss/translations/ja.json | 2 +- .../components/izone/translations/ja.json | 2 +- .../components/jellyfin/translations/ja.json | 2 +- .../components/jellyfin/translations/pt.json | 2 +- .../kaleidescape/translations/pt.json | 11 ++++++++ .../keenetic_ndms2/translations/pt.json | 15 +++++++++++ .../components/knx/translations/ja.json | 2 +- .../components/knx/translations/pt.json | 3 ++- .../components/kodi/translations/pt.json | 2 +- .../components/konnected/translations/pt.json | 4 +-- .../kostal_plenticore/translations/pt.json | 15 +++++++++++ .../components/kraken/translations/ja.json | 2 +- .../components/kraken/translations/pt.json | 12 +++++++++ .../components/kulersky/translations/ja.json | 2 +- .../launch_library/translations/ja.json | 2 +- .../launch_library/translations/pt.json | 7 ++++++ .../components/laundrify/translations/ja.json | 2 +- .../lg_soundbar/translations/pt.json | 17 +++++++++++++ .../components/life360/translations/pt.json | 9 +++++++ .../components/lifx/translations/ja.json | 2 +- .../components/lifx/translations/pt.json | 2 +- .../components/litejet/translations/ja.json | 2 +- .../litterrobot/translations/pt.json | 2 +- .../components/local_ip/translations/ja.json | 2 +- .../components/locative/translations/ja.json | 2 +- .../components/lookin/translations/pt.json | 11 ++++++++ .../components/luftdaten/translations/pt.json | 2 +- .../lutron_caseta/translations/pt.json | 7 ++++++ .../components/lyric/translations/pt.json | 13 ++++++++++ .../components/mailgun/translations/ja.json | 2 +- .../components/mazda/translations/pt.json | 10 ++++++++ .../components/meater/translations/pt.json | 19 ++++++++++++++ .../met_eireann/translations/pt.json | 13 ++++++++++ .../meteoclimatic/translations/pt.json | 3 ++- .../components/mjpeg/translations/pt.json | 23 +++++++++++++++++ .../modem_callerid/translations/pt.json | 7 ++++++ .../modern_forms/translations/pt.json | 11 ++++++++ .../moehlenhoff_alpha2/translations/pt.json | 7 ++++++ .../components/moon/translations/ja.json | 2 +- .../motion_blinds/translations/pt.json | 2 +- .../components/motioneye/translations/pt.json | 14 +++++++++++ .../components/mqtt/translations/ja.json | 2 +- .../components/mqtt/translations/pt.json | 4 +-- .../components/mysensors/translations/pt.json | 8 ++++++ .../components/nam/translations/pt.json | 25 +++++++++++++++++++ .../components/nanoleaf/translations/pt.json | 7 ++++++ .../components/nest/translations/ja.json | 2 +- .../components/nest/translations/pt.json | 12 ++++++++- .../components/netatmo/translations/ja.json | 2 +- .../components/netatmo/translations/pt.json | 4 +++ .../components/netgear/translations/pt.json | 7 ++++++ .../components/nextdns/translations/pt.json | 7 +++++- .../nfandroidtv/translations/pt.json | 15 +++++++++++ .../components/nina/translations/ja.json | 2 +- .../components/nina/translations/pt.json | 13 ++++++++++ .../components/notion/translations/pt.json | 6 +++++ .../components/nuki/translations/pt.json | 11 ++++++++ .../components/nws/translations/pt.json | 2 +- .../components/nzbget/translations/ja.json | 2 +- .../components/nzbget/translations/pt.json | 2 +- .../components/octoprint/translations/pt.json | 10 ++++++++ .../components/omnilogic/translations/ja.json | 2 +- .../components/oncue/translations/pt.json | 7 ++++++ .../ondilo_ico/translations/pt.json | 16 ++++++++++++ .../components/onewire/translations/pt.json | 7 ++++++ .../components/onvif/translations/pt.json | 7 +++++- .../opengarage/translations/pt.json | 15 +++++++++++ .../components/openuv/translations/pt.json | 2 +- .../components/overkiz/translations/pt.json | 14 +++++++++++ .../components/owntracks/translations/ja.json | 2 +- .../p1_monitor/translations/pt.json | 15 +++++++++++ .../philips_js/translations/pt.json | 9 ++++++- .../components/pi_hole/translations/pt.json | 5 ++++ .../components/picnic/translations/pt.json | 18 +++++++++++++ .../components/plaato/translations/ja.json | 2 +- .../components/plaato/translations/pt.json | 2 +- .../components/plex/translations/pt.json | 2 +- .../components/point/translations/ja.json | 2 +- .../components/point/translations/pt.json | 4 +-- .../components/powerwall/translations/pt.json | 8 +++++- .../components/profiler/translations/ja.json | 2 +- .../components/prosegur/translations/pt.json | 5 ++++ .../components/ps4/translations/pt.json | 2 +- .../pure_energie/translations/pt.json | 11 ++++++++ .../components/pvoutput/translations/pt.json | 7 ++++++ .../components/qnap_qsw/translations/pt.json | 21 ++++++++++++++++ .../radio_browser/translations/ja.json | 2 +- .../radio_browser/translations/pt.json | 7 ++++++ .../radiotherm/translations/pt.json | 10 ++++++++ .../rainforest_eagle/translations/pt.json | 8 +++++- .../components/rdw/translations/pt.json | 7 ++++++ .../components/renault/translations/pt.json | 17 +++++++++++++ .../components/rfxtrx/translations/ja.json | 2 +- .../components/rhasspy/translations/ca.json | 7 ++++++ .../components/rhasspy/translations/de.json | 12 +++++++++ .../components/rhasspy/translations/el.json | 12 +++++++++ .../components/rhasspy/translations/ja.json | 12 +++++++++ .../components/rhasspy/translations/pt.json | 7 ++++++ .../rhasspy/translations/zh-Hant.json | 12 +++++++++ .../components/ridwell/translations/pt.json | 23 +++++++++++++++++ .../components/roomba/translations/pt.json | 7 +++++- .../components/roon/translations/pt.json | 7 ++++++ .../components/rpi_power/translations/ja.json | 2 +- .../rtsp_to_webrtc/translations/ja.json | 2 +- .../rtsp_to_webrtc/translations/pt.json | 7 ++++++ .../components/sabnzbd/translations/pt.json | 11 ++++++++ .../components/samsungtv/translations/pt.json | 2 +- .../components/scrape/translations/pt.json | 14 +++++++++++ .../screenlogic/translations/pt.json | 14 +++++++++++ .../components/sense/translations/pt.json | 3 +++ .../components/senseme/translations/pt.json | 14 +++++++++++ .../components/sensibo/translations/pt.json | 19 ++++++++++++++ .../components/sentry/translations/ja.json | 2 +- .../shopping_list/translations/pt.json | 2 +- .../components/sia/translations/pt.json | 11 ++++++++ .../simplepush/translations/pt.json | 17 +++++++++++++ .../components/skybell/translations/pt.json | 15 +++++++++++ .../components/slack/translations/pt.json | 14 +++++++++++ .../components/sleepiq/translations/pt.json | 20 +++++++++++++++ .../components/slimproto/translations/ja.json | 2 +- .../components/slimproto/translations/pt.json | 7 ++++++ .../smartthings/translations/pt.json | 2 +- .../components/smarttub/translations/pt.json | 2 +- .../components/smhi/translations/pt.json | 3 +++ .../components/sms/translations/ja.json | 2 +- .../components/solax/translations/pt.json | 14 +++++++++++ .../components/soma/translations/ja.json | 2 +- .../components/soma/translations/pt.json | 7 ++++++ .../somfy_mylink/translations/pt.json | 17 +++++++++++++ .../components/sonos/translations/ja.json | 2 +- .../components/sonos/translations/pt.json | 2 +- .../soundtouch/translations/pt.json | 17 +++++++++++++ .../speedtestdotnet/translations/ja.json | 2 +- .../components/spider/translations/ja.json | 2 +- .../srp_energy/translations/ja.json | 2 +- .../steam_online/translations/pt.json | 10 ++++++++ .../components/steamist/translations/pt.json | 17 +++++++++++++ .../components/sun/translations/ja.json | 2 +- .../components/sun/translations/pt.json | 7 ++++++ .../surepetcare/translations/pt.json | 16 ++++++++++++ .../components/switchbot/translations/pt.json | 11 ++++++++ .../switcher_kis/translations/ja.json | 2 +- .../components/syncthing/translations/pt.json | 17 +++++++++++++ .../synology_dsm/translations/pt.json | 6 +++++ .../system_bridge/translations/pt.json | 10 ++++++++ .../tankerkoenig/translations/pt.json | 8 ++++++ .../components/tasmota/translations/ja.json | 2 +- .../components/tautulli/translations/ja.json | 2 +- .../components/tautulli/translations/pt.json | 20 +++++++++++++++ .../tellduslive/translations/pt.json | 2 +- .../tesla_wall_connector/translations/pt.json | 14 +++++++++++ .../components/tile/translations/pt.json | 7 +++++- .../tomorrowio/translations/pt.json | 15 +++++++++++ .../components/tplink/translations/pt.json | 3 +++ .../components/traccar/translations/ja.json | 2 +- .../trafikverket_ferry/translations/pt.json | 14 +++++++++++ .../translations/pt.json | 14 +++++++++++ .../transmission/translations/pt.json | 9 ++++++- .../tuya/translations/select.pt.json | 10 +++++++- .../components/twilio/translations/ja.json | 2 +- .../components/unifi/translations/pt.json | 2 +- .../unifiprotect/translations/pt.json | 25 +++++++++++++++++++ .../components/upnp/translations/pt.json | 2 +- .../components/uptime/translations/ja.json | 2 +- .../uptimerobot/translations/pt.json | 15 ++++++++++- .../components/vallox/translations/pt.json | 3 +++ .../components/venstar/translations/pt.json | 18 +++++++++++++ .../components/vesync/translations/ja.json | 2 +- .../components/vicare/translations/ja.json | 2 +- .../components/vicare/translations/pt.json | 18 +++++++++++++ .../vlc_telnet/translations/pt.json | 19 ++++++++++++++ .../components/wallbox/translations/pt.json | 7 ++++++ .../components/watttime/translations/pt.json | 5 +++- .../waze_travel_time/translations/pt.json | 7 ++++++ .../components/weather/translations/pt.json | 4 +-- .../components/webostv/translations/pt.json | 11 ++++++++ .../components/wemo/translations/ja.json | 2 +- .../components/whirlpool/translations/pt.json | 7 ++++++ .../components/whois/translations/pt.json | 7 ++++++ .../components/withings/translations/el.json | 4 +++ .../components/withings/translations/ja.json | 4 +++ .../components/withings/translations/pt.json | 3 +++ .../components/wiz/translations/pt.json | 11 ++++++++ .../wled/translations/select.pt.json | 8 ++++++ .../components/ws66i/translations/pt.json | 7 ++++++ .../components/xbox/translations/ja.json | 2 +- .../xiaomi_miio/translations/pt.json | 8 ++++++ .../yale_smart_alarm/translations/pt.json | 18 +++++++++++++ .../components/yolink/translations/pt.json | 16 ++++++++++++ .../components/youless/translations/pt.json | 7 ++++++ .../components/zerproc/translations/ja.json | 2 +- .../components/zha/translations/ja.json | 2 +- .../components/zha/translations/ru.json | 8 +++--- .../components/zwave_js/translations/pt.json | 22 ++++++++++++++++ .../components/zwave_me/translations/pt.json | 15 +++++++++++ 300 files changed, 2217 insertions(+), 143 deletions(-) create mode 100644 homeassistant/components/adax/translations/pt.json create mode 100644 homeassistant/components/aemet/translations/pt.json create mode 100644 homeassistant/components/airthings/translations/pt.json create mode 100644 homeassistant/components/airzone/translations/pt.json create mode 100644 homeassistant/components/aladdin_connect/translations/pt.json create mode 100644 homeassistant/components/androidtv/translations/pt.json create mode 100644 homeassistant/components/anthemav/translations/pt.json create mode 100644 homeassistant/components/aseko_pool_live/translations/pt.json create mode 100644 homeassistant/components/asuswrt/translations/pt.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/pt.json create mode 100644 homeassistant/components/aussie_broadband/translations/pt.json create mode 100644 homeassistant/components/azure_event_hub/translations/pt.json create mode 100644 homeassistant/components/baf/translations/pt.json create mode 100644 homeassistant/components/balboa/translations/pt.json create mode 100644 homeassistant/components/bosch_shc/translations/pt.json create mode 100644 homeassistant/components/brunt/translations/pt.json create mode 100644 homeassistant/components/buienradar/translations/pt.json create mode 100644 homeassistant/components/co2signal/translations/pt.json create mode 100644 homeassistant/components/coinbase/translations/pt.json create mode 100644 homeassistant/components/cpuspeed/translations/pt.json create mode 100644 homeassistant/components/crownstone/translations/pt.json create mode 100644 homeassistant/components/deluge/translations/pt.json create mode 100644 homeassistant/components/discord/translations/pt.json create mode 100644 homeassistant/components/dlna_dmr/translations/pt.json create mode 100644 homeassistant/components/dlna_dms/translations/pt.json create mode 100644 homeassistant/components/efergy/translations/pt.json create mode 100644 homeassistant/components/eight_sleep/translations/pt.json create mode 100644 homeassistant/components/elmax/translations/pt.json create mode 100644 homeassistant/components/emonitor/translations/pt.json create mode 100644 homeassistant/components/enphase_envoy/translations/pt.json create mode 100644 homeassistant/components/environment_canada/translations/pt.json create mode 100644 homeassistant/components/fibaro/translations/pt.json create mode 100644 homeassistant/components/fivem/translations/pt.json create mode 100644 homeassistant/components/flux_led/translations/pt.json create mode 100644 homeassistant/components/forecast_solar/translations/pt.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/pt.json create mode 100644 homeassistant/components/fronius/translations/pt.json create mode 100644 homeassistant/components/generic/translations/pt.json create mode 100644 homeassistant/components/geocaching/translations/pt.json create mode 100644 homeassistant/components/github/translations/pt.json create mode 100644 homeassistant/components/google/translations/pt.json create mode 100644 homeassistant/components/google_travel_time/translations/pt.json create mode 100644 homeassistant/components/growatt_server/translations/pt.json create mode 100644 homeassistant/components/honeywell/translations/pt.json create mode 100644 homeassistant/components/ialarm/translations/pt.json create mode 100644 homeassistant/components/intellifire/translations/pt.json create mode 100644 homeassistant/components/kaleidescape/translations/pt.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/pt.json create mode 100644 homeassistant/components/kostal_plenticore/translations/pt.json create mode 100644 homeassistant/components/kraken/translations/pt.json create mode 100644 homeassistant/components/launch_library/translations/pt.json create mode 100644 homeassistant/components/lg_soundbar/translations/pt.json create mode 100644 homeassistant/components/lookin/translations/pt.json create mode 100644 homeassistant/components/lyric/translations/pt.json create mode 100644 homeassistant/components/mazda/translations/pt.json create mode 100644 homeassistant/components/meater/translations/pt.json create mode 100644 homeassistant/components/met_eireann/translations/pt.json create mode 100644 homeassistant/components/mjpeg/translations/pt.json create mode 100644 homeassistant/components/modem_callerid/translations/pt.json create mode 100644 homeassistant/components/modern_forms/translations/pt.json create mode 100644 homeassistant/components/moehlenhoff_alpha2/translations/pt.json create mode 100644 homeassistant/components/motioneye/translations/pt.json create mode 100644 homeassistant/components/mysensors/translations/pt.json create mode 100644 homeassistant/components/nam/translations/pt.json create mode 100644 homeassistant/components/nanoleaf/translations/pt.json create mode 100644 homeassistant/components/netgear/translations/pt.json create mode 100644 homeassistant/components/nfandroidtv/translations/pt.json create mode 100644 homeassistant/components/nuki/translations/pt.json create mode 100644 homeassistant/components/octoprint/translations/pt.json create mode 100644 homeassistant/components/oncue/translations/pt.json create mode 100644 homeassistant/components/ondilo_ico/translations/pt.json create mode 100644 homeassistant/components/opengarage/translations/pt.json create mode 100644 homeassistant/components/overkiz/translations/pt.json create mode 100644 homeassistant/components/p1_monitor/translations/pt.json create mode 100644 homeassistant/components/picnic/translations/pt.json create mode 100644 homeassistant/components/pure_energie/translations/pt.json create mode 100644 homeassistant/components/pvoutput/translations/pt.json create mode 100644 homeassistant/components/qnap_qsw/translations/pt.json create mode 100644 homeassistant/components/radio_browser/translations/pt.json create mode 100644 homeassistant/components/radiotherm/translations/pt.json create mode 100644 homeassistant/components/rdw/translations/pt.json create mode 100644 homeassistant/components/renault/translations/pt.json create mode 100644 homeassistant/components/rhasspy/translations/ca.json create mode 100644 homeassistant/components/rhasspy/translations/de.json create mode 100644 homeassistant/components/rhasspy/translations/el.json create mode 100644 homeassistant/components/rhasspy/translations/ja.json create mode 100644 homeassistant/components/rhasspy/translations/pt.json create mode 100644 homeassistant/components/rhasspy/translations/zh-Hant.json create mode 100644 homeassistant/components/ridwell/translations/pt.json create mode 100644 homeassistant/components/rtsp_to_webrtc/translations/pt.json create mode 100644 homeassistant/components/sabnzbd/translations/pt.json create mode 100644 homeassistant/components/scrape/translations/pt.json create mode 100644 homeassistant/components/screenlogic/translations/pt.json create mode 100644 homeassistant/components/senseme/translations/pt.json create mode 100644 homeassistant/components/sensibo/translations/pt.json create mode 100644 homeassistant/components/sia/translations/pt.json create mode 100644 homeassistant/components/simplepush/translations/pt.json create mode 100644 homeassistant/components/skybell/translations/pt.json create mode 100644 homeassistant/components/slack/translations/pt.json create mode 100644 homeassistant/components/sleepiq/translations/pt.json create mode 100644 homeassistant/components/slimproto/translations/pt.json create mode 100644 homeassistant/components/solax/translations/pt.json create mode 100644 homeassistant/components/somfy_mylink/translations/pt.json create mode 100644 homeassistant/components/soundtouch/translations/pt.json create mode 100644 homeassistant/components/steam_online/translations/pt.json create mode 100644 homeassistant/components/steamist/translations/pt.json create mode 100644 homeassistant/components/surepetcare/translations/pt.json create mode 100644 homeassistant/components/switchbot/translations/pt.json create mode 100644 homeassistant/components/syncthing/translations/pt.json create mode 100644 homeassistant/components/system_bridge/translations/pt.json create mode 100644 homeassistant/components/tautulli/translations/pt.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/pt.json create mode 100644 homeassistant/components/tomorrowio/translations/pt.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/pt.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/pt.json create mode 100644 homeassistant/components/unifiprotect/translations/pt.json create mode 100644 homeassistant/components/venstar/translations/pt.json create mode 100644 homeassistant/components/vicare/translations/pt.json create mode 100644 homeassistant/components/vlc_telnet/translations/pt.json create mode 100644 homeassistant/components/wallbox/translations/pt.json create mode 100644 homeassistant/components/waze_travel_time/translations/pt.json create mode 100644 homeassistant/components/webostv/translations/pt.json create mode 100644 homeassistant/components/whirlpool/translations/pt.json create mode 100644 homeassistant/components/whois/translations/pt.json create mode 100644 homeassistant/components/wiz/translations/pt.json create mode 100644 homeassistant/components/wled/translations/select.pt.json create mode 100644 homeassistant/components/ws66i/translations/pt.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/pt.json create mode 100644 homeassistant/components/yolink/translations/pt.json create mode 100644 homeassistant/components/youless/translations/pt.json create mode 100644 homeassistant/components/zwave_js/translations/pt.json create mode 100644 homeassistant/components/zwave_me/translations/pt.json diff --git a/homeassistant/components/abode/translations/ja.json b/homeassistant/components/abode/translations/ja.json index cd498691f4b..1eb5f1cc499 100644 --- a/homeassistant/components/abode/translations/ja.json +++ b/homeassistant/components/abode/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/abode/translations/pt.json b/homeassistant/components/abode/translations/pt.json index 95a51741222..3d6b007b471 100644 --- a/homeassistant/components/abode/translations/pt.json +++ b/homeassistant/components/abode/translations/pt.json @@ -18,7 +18,7 @@ "user": { "data": { "password": "Palavra-passe", - "username": "Endere\u00e7o de e-mail" + "username": "Email" } } } diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index c7ea9f1d264..b9c7819e78a 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u69cb\u6210\u5f8c\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u305d\u308c\u3089\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002" diff --git a/homeassistant/components/adax/translations/pt.json b/homeassistant/components/adax/translations/pt.json new file mode 100644 index 00000000000..b83b23758af --- /dev/null +++ b/homeassistant/components/adax/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "cloud": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index df9b6c03bc5..e389261748d 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, @@ -14,7 +17,7 @@ "port": "Porta", "ssl": "Utiliza um certificado SSL", "username": "Nome de Utilizador", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/aemet/translations/pt.json b/homeassistant/components/aemet/translations/pt.json new file mode 100644 index 00000000000..cc227afe3a3 --- /dev/null +++ b/homeassistant/components/aemet/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/pt.json b/homeassistant/components/airthings/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/airthings/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/pt.json b/homeassistant/components/airvisual/translations/pt.json index cc1c500946d..75869241f0d 100644 --- a/homeassistant/components/airvisual/translations/pt.json +++ b/homeassistant/components/airvisual/translations/pt.json @@ -10,6 +10,11 @@ "invalid_api_key": "Chave de API inv\u00e1lida" }, "step": { + "geography_by_coords": { + "data": { + "latitude": "Latitude" + } + }, "node_pro": { "data": { "ip_address": "Servidor", @@ -18,7 +23,7 @@ }, "reauth_confirm": { "data": { - "api_key": "" + "api_key": "Chave da API" } } } diff --git a/homeassistant/components/airzone/translations/pt.json b/homeassistant/components/airzone/translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/airzone/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/pt.json b/homeassistant/components/aladdin_connect/translations/pt.json new file mode 100644 index 00000000000..6c09ed1c852 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json index 1d41aa41f87..898bc4bc1c0 100644 --- a/homeassistant/components/almond/translations/ja.json +++ b/homeassistant/components/almond/translations/ja.json @@ -4,7 +4,7 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambee/translations/pt.json b/homeassistant/components/ambee/translations/pt.json index 286cd58dd89..4a6d267473b 100644 --- a/homeassistant/components/ambee/translations/pt.json +++ b/homeassistant/components/ambee/translations/pt.json @@ -1,8 +1,14 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, "user": { "data": { + "latitude": "Latitude", "name": "Nome" } } diff --git a/homeassistant/components/ambient_station/translations/pt.json b/homeassistant/components/ambient_station/translations/pt.json index c67faa25f0b..f40c2b211a9 100644 --- a/homeassistant/components/ambient_station/translations/pt.json +++ b/homeassistant/components/ambient_station/translations/pt.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Chave de API", + "api_key": "Chave da API", "app_key": "Chave de aplica\u00e7\u00e3o" }, "title": "Preencha as suas informa\u00e7\u00f5es" diff --git a/homeassistant/components/androidtv/translations/pt.json b/homeassistant/components/androidtv/translations/pt.json new file mode 100644 index 00000000000..09a78c773cc --- /dev/null +++ b/homeassistant/components/androidtv/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt.json b/homeassistant/components/anthemav/translations/pt.json new file mode 100644 index 00000000000..fa5aa3de317 --- /dev/null +++ b/homeassistant/components/anthemav/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/pt.json b/homeassistant/components/aseko_pool_live/translations/pt.json new file mode 100644 index 00000000000..2933743c867 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/pt.json b/homeassistant/components/asuswrt/translations/pt.json new file mode 100644 index 00000000000..54c86ef332a --- /dev/null +++ b/homeassistant/components/asuswrt/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta (leave empty for protocol default)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/pt.json b/homeassistant/components/aurora_abb_powerone/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/pt.json b/homeassistant/components/aussie_broadband/translations/pt.json new file mode 100644 index 00000000000..e831e2ce397 --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + }, + "options": { + "abort": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index ea99bbf0167..c906e6f380e 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -16,6 +16,12 @@ "email": "Email" } }, + "reauth_confirm": { + "data": { + "access_token": "Token de Acesso", + "email": "Email" + } + }, "user": { "data": { "access_token": "Token de Acesso", diff --git a/homeassistant/components/azure_event_hub/translations/ja.json b/homeassistant/components/azure_event_hub/translations/ja.json index b504f86a3bd..720e57d8066 100644 --- a/homeassistant/components/azure_event_hub/translations/ja.json +++ b/homeassistant/components/azure_event_hub/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3067\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { diff --git a/homeassistant/components/azure_event_hub/translations/pt.json b/homeassistant/components/azure_event_hub/translations/pt.json new file mode 100644 index 00000000000..d252c078a2c --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/pt.json b/homeassistant/components/baf/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/baf/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/pt.json b/homeassistant/components/balboa/translations/pt.json new file mode 100644 index 00000000000..f13cad90edc --- /dev/null +++ b/homeassistant/components/balboa/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index d204347bd02..a91c4987abb 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -105,6 +105,9 @@ "off": "Sem carregar", "on": "A carregar" }, + "carbon_monoxide": { + "on": "Detectado" + }, "cold": { "off": "Normal", "on": "Frio" @@ -186,7 +189,7 @@ }, "vibration": { "off": "Limpo", - "on": "Detetado" + "on": "Detectado" }, "window": { "off": "Fechada", diff --git a/homeassistant/components/blink/translations/pt.json b/homeassistant/components/blink/translations/pt.json index 76c420a584c..1f71ecf3f22 100644 --- a/homeassistant/components/blink/translations/pt.json +++ b/homeassistant/components/blink/translations/pt.json @@ -6,7 +6,8 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_access_token": "Token de acesso inv\u00e1lido", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "step": { "2fa": { diff --git a/homeassistant/components/bosch_shc/translations/pt.json b/homeassistant/components/bosch_shc/translations/pt.json new file mode 100644 index 00000000000..1286d95f2df --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index e113d74d6fc..0838bb5d632 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -4,7 +4,7 @@ "already_configured": "Esta TV j\u00e1 est\u00e1 configurada." }, "error": { - "cannot_connect": "Falha na conex\u00e3o, nome de servidor inv\u00e1lido ou c\u00f3digo PIN.", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", "unsupported_model": "O seu modelo de TV n\u00e3o \u00e9 suportado." }, diff --git a/homeassistant/components/brunt/translations/pt.json b/homeassistant/components/brunt/translations/pt.json new file mode 100644 index 00000000000..a27df267700 --- /dev/null +++ b/homeassistant/components/brunt/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/pt.json b/homeassistant/components/bsblan/translations/pt.json index 5461f207375..3cb7a7d5891 100644 --- a/homeassistant/components/bsblan/translations/pt.json +++ b/homeassistant/components/bsblan/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" diff --git a/homeassistant/components/buienradar/translations/pt.json b/homeassistant/components/buienradar/translations/pt.json new file mode 100644 index 00000000000..2e6515edd09 --- /dev/null +++ b/homeassistant/components/buienradar/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/ja.json b/homeassistant/components/canary/translations/ja.json index 9f9903b86e4..1fcbcde47d1 100644 --- a/homeassistant/components/canary/translations/ja.json +++ b/homeassistant/components/canary/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index 626ef56cba1..1dab27d9e54 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8\u306f\u3001\u30b3\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305f\u30db\u30b9\u30c8\u306e\u30ea\u30b9\u30c8\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002" diff --git a/homeassistant/components/cast/translations/pt.json b/homeassistant/components/cast/translations/pt.json index bb29c923128..34770733822 100644 --- a/homeassistant/components/cast/translations/pt.json +++ b/homeassistant/components/cast/translations/pt.json @@ -8,7 +8,7 @@ "title": "Google Cast" }, "confirm": { - "description": "Deseja configurar o Google Cast?" + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" } } } diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 1057d4e7bc5..08ffdd5c7a5 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/cloudflare/translations/pt.json b/homeassistant/components/cloudflare/translations/pt.json index 158cd3f3f74..650823693d6 100644 --- a/homeassistant/components/cloudflare/translations/pt.json +++ b/homeassistant/components/cloudflare/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/co2signal/translations/pt.json b/homeassistant/components/co2signal/translations/pt.json new file mode 100644 index 00000000000..cf6bc7f5bc4 --- /dev/null +++ b/homeassistant/components/co2signal/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/pt.json b/homeassistant/components/coinbase/translations/pt.json new file mode 100644 index 00000000000..49cb628dd85 --- /dev/null +++ b/homeassistant/components/coinbase/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cpuspeed/translations/ja.json b/homeassistant/components/cpuspeed/translations/ja.json index 12cec097b11..95e92eafa76 100644 --- a/homeassistant/components/cpuspeed/translations/ja.json +++ b/homeassistant/components/cpuspeed/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "not_compatible": "CPU\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u7d71\u5408\u306f\u3001\u304a\u4f7f\u3044\u306e\u30b7\u30b9\u30c6\u30e0\u3068\u4e92\u63db\u6027\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, "step": { diff --git a/homeassistant/components/cpuspeed/translations/pt.json b/homeassistant/components/cpuspeed/translations/pt.json new file mode 100644 index 00000000000..cf03f249d96 --- /dev/null +++ b/homeassistant/components/cpuspeed/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/pt.json b/homeassistant/components/crownstone/translations/pt.json new file mode 100644 index 00000000000..97ea705b32b --- /dev/null +++ b/homeassistant/components/crownstone/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "usb_manual_config": { + "data": { + "usb_manual_path": "Caminho do Dispositivo USB" + } + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/pt.json b/homeassistant/components/deluge/translations/pt.json new file mode 100644 index 00000000000..fb1af357526 --- /dev/null +++ b/homeassistant/components/deluge/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/pt.json b/homeassistant/components/derivative/translations/pt.json index d6c0f4acd0b..6801ab6b6d4 100644 --- a/homeassistant/components/derivative/translations/pt.json +++ b/homeassistant/components/derivative/translations/pt.json @@ -7,5 +7,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data_description": { + "unit_prefix": "." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/pt.json b/homeassistant/components/devolo_home_control/translations/pt.json index 2215d148b7b..d60cc81f541 100644 --- a/homeassistant/components/devolo_home_control/translations/pt.json +++ b/homeassistant/components/devolo_home_control/translations/pt.json @@ -13,6 +13,11 @@ "password": "Palavra-passe", "username": "Email / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "password": "Palavra-passe" + } } } } diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json index 27e5d35e35e..8ba9292c8cd 100644 --- a/homeassistant/components/dialogflow/translations/ja.json +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/discord/translations/pt.json b/homeassistant/components/discord/translations/pt.json new file mode 100644 index 00000000000..9925c2a7416 --- /dev/null +++ b/homeassistant/components/discord/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token" + } + }, + "user": { + "data": { + "api_token": "API Token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/pt.json b/homeassistant/components/dlna_dmr/translations/pt.json new file mode 100644 index 00000000000..f6f3f84e497 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "manual": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/pt.json b/homeassistant/components/dlna_dms/translations/pt.json new file mode 100644 index 00000000000..f67e36f3487 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/pt.json b/homeassistant/components/dsmr/translations/pt.json index ce8a9287272..c9fff00bd53 100644 --- a/homeassistant/components/dsmr/translations/pt.json +++ b/homeassistant/components/dsmr/translations/pt.json @@ -1,7 +1,23 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "setup_network": { + "data": { + "host": "Servidor" + } + }, + "setup_serial_manual_path": { + "data": { + "port": "Caminho do Dispositivo USB" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/ja.json b/homeassistant/components/ecobee/translations/ja.json index 73ac4cd1611..79f8add6a4d 100644 --- a/homeassistant/components/ecobee/translations/ja.json +++ b/homeassistant/components/ecobee/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "pin_request_failed": "ecobee\u304b\u3089\u306ePIN\u30ea\u30af\u30a8\u30b9\u30c8\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f; API\u30ad\u30fc\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/econet/translations/pt.json b/homeassistant/components/econet/translations/pt.json index ce8a9287272..6c333b17fc9 100644 --- a/homeassistant/components/econet/translations/pt.json +++ b/homeassistant/components/econet/translations/pt.json @@ -1,7 +1,15 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/pt.json b/homeassistant/components/efergy/translations/pt.json new file mode 100644 index 00000000000..8f319572c97 --- /dev/null +++ b/homeassistant/components/efergy/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/pt.json b/homeassistant/components/eight_sleep/translations/pt.json new file mode 100644 index 00000000000..bcb163d8c10 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/pt.json b/homeassistant/components/elkm1/translations/pt.json index 2e669c21f1e..6b770832a24 100644 --- a/homeassistant/components/elkm1/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt.json @@ -4,6 +4,19 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "discovered_connection": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "manual_connection": { + "data": { + "username": "Nome de Utilizador" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/pt.json b/homeassistant/components/elmax/translations/pt.json new file mode 100644 index 00000000000..bcb163d8c10 --- /dev/null +++ b/homeassistant/components/elmax/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/pt.json b/homeassistant/components/emonitor/translations/pt.json new file mode 100644 index 00000000000..8578f969852 --- /dev/null +++ b/homeassistant/components/emonitor/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/ja.json b/homeassistant/components/enocean/translations/ja.json index e0ec74d778f..bc71afe3a27 100644 --- a/homeassistant/components/enocean/translations/ja.json +++ b/homeassistant/components/enocean/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_dongle_path": "\u30c9\u30f3\u30b0\u30eb\u30d1\u30b9\u304c\u7121\u52b9", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_dongle_path": "\u3053\u306e\u30d1\u30b9\u306b\u6709\u52b9\u306a\u30c9\u30f3\u30b0\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" diff --git a/homeassistant/components/enphase_envoy/translations/pt.json b/homeassistant/components/enphase_envoy/translations/pt.json new file mode 100644 index 00000000000..1aa61659a93 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/pt.json b/homeassistant/components/environment_canada/translations/pt.json new file mode 100644 index 00000000000..c7081cd694a --- /dev/null +++ b/homeassistant/components/environment_canada/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index 60eeaa3f4b2..da3216186ea 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", diff --git a/homeassistant/components/evil_genius_labs/translations/pt.json b/homeassistant/components/evil_genius_labs/translations/pt.json index 4e8578a0a28..f13cad90edc 100644 --- a/homeassistant/components/evil_genius_labs/translations/pt.json +++ b/homeassistant/components/evil_genius_labs/translations/pt.json @@ -1,9 +1,12 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { - "host": "Anfitri\u00e3o" + "host": "Servidor" } } } diff --git a/homeassistant/components/ezviz/translations/pt.json b/homeassistant/components/ezviz/translations/pt.json index cd669c3fd29..b7e8b2d8b81 100644 --- a/homeassistant/components/ezviz/translations/pt.json +++ b/homeassistant/components/ezviz/translations/pt.json @@ -5,6 +5,11 @@ "data": { "password": "Palavra-passe" } + }, + "user_custom_url": { + "data": { + "username": "Nome de Utilizador" + } } } } diff --git a/homeassistant/components/fibaro/translations/pt.json b/homeassistant/components/fibaro/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/fibaro/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/pt.json b/homeassistant/components/firmata/translations/pt.json index 785f8887678..32a5bb07569 100644 --- a/homeassistant/components/firmata/translations/pt.json +++ b/homeassistant/components/firmata/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "N\u0101o foi poss\u00edvel conectar ao board do Firmata durante a configura\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { "one": "Um", diff --git a/homeassistant/components/fivem/translations/pt.json b/homeassistant/components/fivem/translations/pt.json new file mode 100644 index 00000000000..cf73866f4a0 --- /dev/null +++ b/homeassistant/components/fivem/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown_error": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "name": "Nome", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/ja.json b/homeassistant/components/fjaraskupan/translations/ja.json index f22b3c04c84..e7c39f8d142 100644 --- a/homeassistant/components/fjaraskupan/translations/ja.json +++ b/homeassistant/components/fjaraskupan/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/flipr/translations/pt.json b/homeassistant/components/flipr/translations/pt.json index ce1bf4bb4b8..12a3977fdde 100644 --- a/homeassistant/components/flipr/translations/pt.json +++ b/homeassistant/components/flipr/translations/pt.json @@ -1,7 +1,15 @@ { "config": { "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/pt.json b/homeassistant/components/flux_led/translations/pt.json new file mode 100644 index 00000000000..7f2f103180a --- /dev/null +++ b/homeassistant/components/flux_led/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "flow_title": "", + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/pt.json b/homeassistant/components/forecast_solar/translations/pt.json new file mode 100644 index 00000000000..2e6515edd09 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pt.json b/homeassistant/components/foscam/translations/pt.json index 65a6a1558db..f1afd77d051 100644 --- a/homeassistant/components/foscam/translations/pt.json +++ b/homeassistant/components/foscam/translations/pt.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { "data": { + "host": "Servidor", "password": "Palavra-passe" } } diff --git a/homeassistant/components/fritz/translations/pt.json b/homeassistant/components/fritz/translations/pt.json index 9eeb4c35b69..263485b4587 100644 --- a/homeassistant/components/fritz/translations/pt.json +++ b/homeassistant/components/fritz/translations/pt.json @@ -3,6 +3,13 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/pt.json b/homeassistant/components/fritzbox_callmonitor/translations/pt.json new file mode 100644 index 00000000000..0077ceddd46 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/pt.json b/homeassistant/components/fronius/translations/pt.json new file mode 100644 index 00000000000..f13cad90edc --- /dev/null +++ b/homeassistant/components/fronius/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index 499de6409e6..8e1ba58b4df 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/generic/translations/pt.json b/homeassistant/components/generic/translations/pt.json new file mode 100644 index 00000000000..b59623f6607 --- /dev/null +++ b/homeassistant/components/generic/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/pt.json b/homeassistant/components/geocaching/translations/pt.json new file mode 100644 index 00000000000..8685cf2d3c2 --- /dev/null +++ b/homeassistant/components/geocaching/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/ja.json b/homeassistant/components/geofency/translations/ja.json index e653cb99d43..812ba39b940 100644 --- a/homeassistant/components/geofency/translations/ja.json +++ b/homeassistant/components/geofency/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/github/translations/pt.json b/homeassistant/components/github/translations/pt.json new file mode 100644 index 00000000000..d252c078a2c --- /dev/null +++ b/homeassistant/components/github/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/pt.json b/homeassistant/components/goodwe/translations/pt.json index ce8a9287272..410be6f924f 100644 --- a/homeassistant/components/goodwe/translations/pt.json +++ b/homeassistant/components/goodwe/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Endere\u00e7o IP" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/pt.json b/homeassistant/components/google/translations/pt.json new file mode 100644 index 00000000000..518809302e3 --- /dev/null +++ b/homeassistant/components/google/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/pt.json b/homeassistant/components/google_travel_time/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/ja.json b/homeassistant/components/gpslogger/translations/ja.json index 4674c074763..6b05591da9b 100644 --- a/homeassistant/components/gpslogger/translations/ja.json +++ b/homeassistant/components/gpslogger/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/gree/translations/ja.json b/homeassistant/components/gree/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/gree/translations/ja.json +++ b/homeassistant/components/gree/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 728b1dcbd95..b05b2868744 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -3,8 +3,7 @@ "step": { "media_player": { "data": { - "entities": "Membros", - "name": "Nome" + "entities": "Membros" } } } diff --git a/homeassistant/components/growatt_server/translations/pt.json b/homeassistant/components/growatt_server/translations/pt.json new file mode 100644 index 00000000000..a814cc1772d --- /dev/null +++ b/homeassistant/components/growatt_server/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/pt.json b/homeassistant/components/hangouts/translations/pt.json index 093deaecc15..b4feb91c76d 100644 --- a/homeassistant/components/hangouts/translations/pt.json +++ b/homeassistant/components/hangouts/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts j\u00e1 est\u00e1 configurado", - "unknown": "Ocorreu um erro desconhecido." + "unknown": "Erro inesperado" }, "error": { "invalid_2fa": "Autentica\u00e7\u00e3o por 2 fatores inv\u00e1lida, por favor, tente novamente.", diff --git a/homeassistant/components/heos/translations/ja.json b/homeassistant/components/heos/translations/ja.json index 55e075a548a..9a6e9513ecd 100644 --- a/homeassistant/components/heos/translations/ja.json +++ b/homeassistant/components/heos/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/hisense_aehw4a1/translations/ja.json b/homeassistant/components/hisense_aehw4a1/translations/ja.json index 75107c4c0fc..edace613685 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/ja.json +++ b/homeassistant/components/hisense_aehw4a1/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/home_plus_control/translations/ja.json b/homeassistant/components/home_plus_control/translations/ja.json index 3cef1cd5fa1..e1d0ade87b9 100644 --- a/homeassistant/components/home_plus_control/translations/ja.json +++ b/homeassistant/components/home_plus_control/translations/ja.json @@ -6,7 +6,7 @@ "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json index f122a97b19c..4e25e3b691c 100644 --- a/homeassistant/components/homekit/translations/pt.json +++ b/homeassistant/components/homekit/translations/pt.json @@ -22,6 +22,7 @@ }, "init": { "data": { + "domains": "Dom\u00ednios a incluir", "mode": "Modo" }, "title": "Selecione os dom\u00ednios a serem expostos." diff --git a/homeassistant/components/homematicip_cloud/translations/pt.json b/homeassistant/components/homematicip_cloud/translations/pt.json index 645ba242561..2016dc9e13c 100644 --- a/homeassistant/components/homematicip_cloud/translations/pt.json +++ b/homeassistant/components/homematicip_cloud/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O ponto de acesso j\u00e1 se encontra configurado", + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "connection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP", "unknown": "Ocorreu um erro desconhecido." }, diff --git a/homeassistant/components/honeywell/translations/pt.json b/homeassistant/components/honeywell/translations/pt.json new file mode 100644 index 00000000000..ebb712d0682 --- /dev/null +++ b/homeassistant/components/honeywell/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/pt.json b/homeassistant/components/huawei_lte/translations/pt.json index be10fe829f7..c8856eb2f79 100644 --- a/homeassistant/components/huawei_lte/translations/pt.json +++ b/homeassistant/components/huawei_lte/translations/pt.json @@ -13,7 +13,7 @@ "data": { "password": "Palavra-passe", "url": "", - "username": "Nome do utilizador" + "username": "Nome de Utilizador" }, "title": "Configurar o Huawei LTE" } diff --git a/homeassistant/components/huisbaasje/translations/pt.json b/homeassistant/components/huisbaasje/translations/pt.json index 3b5850222d9..a2f32087684 100644 --- a/homeassistant/components/huisbaasje/translations/pt.json +++ b/homeassistant/components/huisbaasje/translations/pt.json @@ -1,7 +1,16 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/pt.json b/homeassistant/components/ialarm/translations/pt.json new file mode 100644 index 00000000000..0c5c7760566 --- /dev/null +++ b/homeassistant/components/ialarm/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/ja.json b/homeassistant/components/iaqualink/translations/ja.json index 20c3551ff80..968f4023522 100644 --- a/homeassistant/components/iaqualink/translations/ja.json +++ b/homeassistant/components/iaqualink/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/iaqualink/translations/pt.json b/homeassistant/components/iaqualink/translations/pt.json index 3b466866334..f73c9be5561 100644 --- a/homeassistant/components/iaqualink/translations/pt.json +++ b/homeassistant/components/iaqualink/translations/pt.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Palavra-passe", - "username": "Nome de utilizador / Endere\u00e7o de e-mail" + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/ifttt/translations/ja.json b/homeassistant/components/ifttt/translations/ja.json index 87844be51d4..05353ba44a1 100644 --- a/homeassistant/components/ifttt/translations/ja.json +++ b/homeassistant/components/ifttt/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 812f9c0bfd6..f5b41709d71 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_insteon_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Insteon\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json index 1475c2d00d3..4654d2c4de1 100644 --- a/homeassistant/components/insteon/translations/pt.json +++ b/homeassistant/components/insteon/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" diff --git a/homeassistant/components/intellifire/translations/pt.json b/homeassistant/components/intellifire/translations/pt.json new file mode 100644 index 00000000000..b86378c5e24 --- /dev/null +++ b/homeassistant/components/intellifire/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "flow_title": "{serial} ({host})", + "step": { + "api_config": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/translations/ja.json b/homeassistant/components/ios/translations/ja.json index 60fbfbea06f..d973d38e176 100644 --- a/homeassistant/components/ios/translations/ja.json +++ b/homeassistant/components/ios/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/ios/translations/pt.json b/homeassistant/components/ios/translations/pt.json index 319ba1e3759..096d42a6503 100644 --- a/homeassistant/components/ios/translations/pt.json +++ b/homeassistant/components/ios/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do componente iOS do Home Assistante \u00e9 necess\u00e1ria." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/iotawatt/translations/pt.json b/homeassistant/components/iotawatt/translations/pt.json index 85b7c4b704a..6e210edaef7 100644 --- a/homeassistant/components/iotawatt/translations/pt.json +++ b/homeassistant/components/iotawatt/translations/pt.json @@ -1,9 +1,17 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "auth": { "data": { - "username": "Nome de utilizador" + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "host": "Servidor" } } } diff --git a/homeassistant/components/islamic_prayer_times/translations/ja.json b/homeassistant/components/islamic_prayer_times/translations/ja.json index ea6ad0c6522..9c38d5a4aab 100644 --- a/homeassistant/components/islamic_prayer_times/translations/ja.json +++ b/homeassistant/components/islamic_prayer_times/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/iss/translations/ja.json b/homeassistant/components/iss/translations/ja.json index bf5deeaa716..d53b9f8fecb 100644 --- a/homeassistant/components/iss/translations/ja.json +++ b/homeassistant/components/iss/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "latitude_longitude_not_defined": "Home Assistant\u3067\u7def\u5ea6\u3068\u7d4c\u5ea6\u304c\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/izone/translations/ja.json b/homeassistant/components/izone/translations/ja.json index bd5ae39dec4..1699170cbd9 100644 --- a/homeassistant/components/izone/translations/ja.json +++ b/homeassistant/components/izone/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/jellyfin/translations/ja.json b/homeassistant/components/jellyfin/translations/ja.json index 69cc9a5279d..fe3ae792913 100644 --- a/homeassistant/components/jellyfin/translations/ja.json +++ b/homeassistant/components/jellyfin/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/jellyfin/translations/pt.json b/homeassistant/components/jellyfin/translations/pt.json index d1f8622ba3f..8e3e0f68479 100644 --- a/homeassistant/components/jellyfin/translations/pt.json +++ b/homeassistant/components/jellyfin/translations/pt.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "password": "Palavra Passe", + "password": "Palavra-passe", "url": "Endere\u00e7o", "username": "Nome de utilizador" } diff --git a/homeassistant/components/kaleidescape/translations/pt.json b/homeassistant/components/kaleidescape/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/pt.json b/homeassistant/components/keenetic_ndms2/translations/pt.json new file mode 100644 index 00000000000..df69b1c4ff3 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index dcd44d5838f..bbac3566bca 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/knx/translations/pt.json b/homeassistant/components/knx/translations/pt.json index ba21368f797..7220ef495c9 100644 --- a/homeassistant/components/knx/translations/pt.json +++ b/homeassistant/components/knx/translations/pt.json @@ -3,7 +3,8 @@ "step": { "tunnel": { "data": { - "host": "Anfitri\u00e3o" + "host": "Anfitri\u00e3o", + "port": "Porta" } } } diff --git a/homeassistant/components/kodi/translations/pt.json b/homeassistant/components/kodi/translations/pt.json index 441d052867f..a20906321aa 100644 --- a/homeassistant/components/kodi/translations/pt.json +++ b/homeassistant/components/kodi/translations/pt.json @@ -25,7 +25,7 @@ "data": { "host": "Servidor", "port": "Porta", - "ssl": "Conecte-se por SSL" + "ssl": "Utiliza um certificado SSL" }, "description": "Informa\u00e7\u00f5es de conex\u00e3o Kodi. Certifique-se de habilitar \"Permitir controle do Kodi via HTTP\" em Sistema / Configura\u00e7\u00f5es / Rede / Servi\u00e7os." }, diff --git a/homeassistant/components/konnected/translations/pt.json b/homeassistant/components/konnected/translations/pt.json index 64aaf6cbf4a..2ba94342093 100644 --- a/homeassistant/components/konnected/translations/pt.json +++ b/homeassistant/components/konnected/translations/pt.json @@ -25,7 +25,7 @@ "step": { "options_binary": { "data": { - "name": "Nome (opcional)" + "name": "Nome" } }, "options_digital": { @@ -55,7 +55,7 @@ }, "options_switch": { "data": { - "name": "Nome (opcional)" + "name": "Nome" } } } diff --git a/homeassistant/components/kostal_plenticore/translations/pt.json b/homeassistant/components/kostal_plenticore/translations/pt.json new file mode 100644 index 00000000000..df69b1c4ff3 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/ja.json b/homeassistant/components/kraken/translations/ja.json index 1d581131252..40a1ac53232 100644 --- a/homeassistant/components/kraken/translations/ja.json +++ b/homeassistant/components/kraken/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/kraken/translations/pt.json b/homeassistant/components/kraken/translations/pt.json new file mode 100644 index 00000000000..66ccff7f372 --- /dev/null +++ b/homeassistant/components/kraken/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/ja.json b/homeassistant/components/kulersky/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/kulersky/translations/ja.json +++ b/homeassistant/components/kulersky/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/launch_library/translations/ja.json b/homeassistant/components/launch_library/translations/ja.json index f0e85fa9a4a..c824577578f 100644 --- a/homeassistant/components/launch_library/translations/ja.json +++ b/homeassistant/components/launch_library/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/launch_library/translations/pt.json b/homeassistant/components/launch_library/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/launch_library/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/ja.json b/homeassistant/components/laundrify/translations/ja.json index 020b7578e80..f80f610ac9a 100644 --- a/homeassistant/components/laundrify/translations/ja.json +++ b/homeassistant/components/laundrify/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/lg_soundbar/translations/pt.json b/homeassistant/components/lg_soundbar/translations/pt.json new file mode 100644 index 00000000000..91ff56e73f8 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/pt.json b/homeassistant/components/life360/translations/pt.json index 71370e40068..cc3b190458f 100644 --- a/homeassistant/components/life360/translations/pt.json +++ b/homeassistant/components/life360/translations/pt.json @@ -1,16 +1,25 @@ { "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "unknown": "Erro inesperado" }, "error": { "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_username": "Nome de utilizador incorreto", "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/lifx/translations/ja.json b/homeassistant/components/lifx/translations/ja.json index 6cfa33a7ace..1945c3112f0 100644 --- a/homeassistant/components/lifx/translations/ja.json +++ b/homeassistant/components/lifx/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/translations/pt.json b/homeassistant/components/lifx/translations/pt.json index 56064a70e0d..594ac7dacc4 100644 --- a/homeassistant/components/lifx/translations/pt.json +++ b/homeassistant/components/lifx/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede.", - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do LIFX \u00e9 permitida." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/litejet/translations/ja.json b/homeassistant/components/litejet/translations/ja.json index c26dc073113..8a7f367f66a 100644 --- a/homeassistant/components/litejet/translations/ja.json +++ b/homeassistant/components/litejet/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "open_failed": "\u6307\u5b9a\u3055\u308c\u305f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002" diff --git a/homeassistant/components/litterrobot/translations/pt.json b/homeassistant/components/litterrobot/translations/pt.json index 7953cf5625c..c2bf0536ccf 100644 --- a/homeassistant/components/litterrobot/translations/pt.json +++ b/homeassistant/components/litterrobot/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Conta j\u00e1 configurada" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", diff --git a/homeassistant/components/local_ip/translations/ja.json b/homeassistant/components/local_ip/translations/ja.json index f5d2efd6613..6ba538c36ce 100644 --- a/homeassistant/components/local_ip/translations/ja.json +++ b/homeassistant/components/local_ip/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/locative/translations/ja.json b/homeassistant/components/locative/translations/ja.json index a4f03bde29f..ee9e6f5f1a1 100644 --- a/homeassistant/components/locative/translations/ja.json +++ b/homeassistant/components/locative/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/lookin/translations/pt.json b/homeassistant/components/lookin/translations/pt.json new file mode 100644 index 00000000000..28ad8b6c8f3 --- /dev/null +++ b/homeassistant/components/lookin/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/pt.json b/homeassistant/components/luftdaten/translations/pt.json index 709ed6af0a1..12e4c078d8c 100644 --- a/homeassistant/components/luftdaten/translations/pt.json +++ b/homeassistant/components/luftdaten/translations/pt.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Mostrar no mapa", - "station_id": "Luftdaten Sensor ID" + "station_id": "Sensor ID" } } } diff --git a/homeassistant/components/lutron_caseta/translations/pt.json b/homeassistant/components/lutron_caseta/translations/pt.json index a04f550a71a..f4fdf676dbd 100644 --- a/homeassistant/components/lutron_caseta/translations/pt.json +++ b/homeassistant/components/lutron_caseta/translations/pt.json @@ -6,6 +6,13 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pt.json b/homeassistant/components/lyric/translations/pt.json new file mode 100644 index 00000000000..002029ae6f7 --- /dev/null +++ b/homeassistant/components/lyric/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/ja.json b/homeassistant/components/mailgun/translations/ja.json index 34c6ced3f38..9c0f731f8fb 100644 --- a/homeassistant/components/mailgun/translations/ja.json +++ b/homeassistant/components/mailgun/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/mazda/translations/pt.json b/homeassistant/components/mazda/translations/pt.json new file mode 100644 index 00000000000..70ac9ae7bf8 --- /dev/null +++ b/homeassistant/components/mazda/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/pt.json b/homeassistant/components/meater/translations/pt.json new file mode 100644 index 00000000000..69bade43888 --- /dev/null +++ b/homeassistant/components/meater/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "unknown_auth_error": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/pt.json b/homeassistant/components/met_eireann/translations/pt.json new file mode 100644 index 00000000000..785a126c56b --- /dev/null +++ b/homeassistant/components/met_eireann/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "name": "Nome" + }, + "title": "Localiza\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/pt.json b/homeassistant/components/meteoclimatic/translations/pt.json index ce8a9287272..245d9637441 100644 --- a/homeassistant/components/meteoclimatic/translations/pt.json +++ b/homeassistant/components/meteoclimatic/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "unknown": "Erro inesperado" } } } \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/pt.json b/homeassistant/components/mjpeg/translations/pt.json new file mode 100644 index 00000000000..1967a9e4a50 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "password": "Palavra-passe" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/pt.json b/homeassistant/components/modem_callerid/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/pt.json b/homeassistant/components/modern_forms/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/pt.json b/homeassistant/components/moehlenhoff_alpha2/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ja.json b/homeassistant/components/moon/translations/ja.json index 6544580781f..f7678a63278 100644 --- a/homeassistant/components/moon/translations/ja.json +++ b/homeassistant/components/moon/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/motion_blinds/translations/pt.json b/homeassistant/components/motion_blinds/translations/pt.json index 7538043f1ce..ccf03b80e43 100644 --- a/homeassistant/components/motion_blinds/translations/pt.json +++ b/homeassistant/components/motion_blinds/translations/pt.json @@ -5,7 +5,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "connection_error": "Falha na liga\u00e7\u00e3o" }, - "flow_title": "Cortinas Motion", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motioneye/translations/pt.json b/homeassistant/components/motioneye/translations/pt.json new file mode 100644 index 00000000000..848cfe1ac2d --- /dev/null +++ b/homeassistant/components/motioneye/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 59dff554676..07af6230aa6 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/mqtt/translations/pt.json b/homeassistant/components/mqtt/translations/pt.json index 209c33cf165..6ff10cf515c 100644 --- a/homeassistant/components/mqtt/translations/pt.json +++ b/homeassistant/components/mqtt/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do MQTT \u00e9 permitida." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel ligar ao broker." @@ -57,7 +57,7 @@ "will_retain": "Reter mensagem testamental", "will_topic": "T\u00f3pico da mensagem testamental" }, - "description": "Por favor, selecione as op\u00e7\u00f5es do MQTT." + "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect." } } } diff --git a/homeassistant/components/mysensors/translations/pt.json b/homeassistant/components/mysensors/translations/pt.json new file mode 100644 index 00000000000..3ace45dd942 --- /dev/null +++ b/homeassistant/components/mysensors/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/pt.json b/homeassistant/components/nam/translations/pt.json new file mode 100644 index 00000000000..0aa6df94840 --- /dev/null +++ b/homeassistant/components/nam/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "credentials": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "reauth_confirm": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/pt.json b/homeassistant/components/nanoleaf/translations/pt.json new file mode 100644 index 00000000000..7293cf1c3c3 --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 398b09f92c1..55d9b9a0348 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -7,7 +7,7 @@ "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "create_entry": { diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 13a7439b93d..40b70a2c67f 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", @@ -14,9 +16,14 @@ "internal_error": "Erro interno ao validar o c\u00f3digo", "invalid_pin": "C\u00f3digo PIN inv\u00e1lido", "timeout": "Limite temporal ultrapassado ao validar c\u00f3digo", - "unknown": "Erro desconhecido ao validar o c\u00f3digo" + "unknown": "Erro inesperado" }, "step": { + "auth": { + "data": { + "code": "Token de Acesso" + } + }, "init": { "data": { "flow_impl": "Fornecedor" @@ -33,6 +40,9 @@ }, "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index 016411aef41..c60eb3dcd5e 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -5,7 +5,7 @@ "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/netatmo/translations/pt.json b/homeassistant/components/netatmo/translations/pt.json index e39ecffa8a7..191337f9812 100644 --- a/homeassistant/components/netatmo/translations/pt.json +++ b/homeassistant/components/netatmo/translations/pt.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "create_entry": { @@ -12,6 +13,9 @@ "step": { "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/netgear/translations/pt.json b/homeassistant/components/netgear/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/netgear/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt.json b/homeassistant/components/nextdns/translations/pt.json index 92f429b6a45..cd774c0c6cd 100644 --- a/homeassistant/components/nextdns/translations/pt.json +++ b/homeassistant/components/nextdns/translations/pt.json @@ -1,9 +1,14 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { - "api_key": "Chave API" + "api_key": "Chave da API" } } } diff --git a/homeassistant/components/nfandroidtv/translations/pt.json b/homeassistant/components/nfandroidtv/translations/pt.json new file mode 100644 index 00000000000..e3233d5779f --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ja.json b/homeassistant/components/nina/translations/ja.json index fa3fa6f4995..5591b2b0986 100644 --- a/homeassistant/components/nina/translations/ja.json +++ b/homeassistant/components/nina/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/nina/translations/pt.json b/homeassistant/components/nina/translations/pt.json index 052bde4e432..f86b761e487 100644 --- a/homeassistant/components/nina/translations/pt.json +++ b/homeassistant/components/nina/translations/pt.json @@ -1,9 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, "step": { "user": { "title": "Selecione a cidade/distrito" } } + }, + "options": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/pt.json b/homeassistant/components/notion/translations/pt.json index e92d51b2058..4ce3e317e32 100644 --- a/homeassistant/components/notion/translations/pt.json +++ b/homeassistant/components/notion/translations/pt.json @@ -7,6 +7,12 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/nuki/translations/pt.json b/homeassistant/components/nuki/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/nuki/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/pt.json b/homeassistant/components/nws/translations/pt.json index 2447be7ee67..3d9fdf5a2d3 100644 --- a/homeassistant/components/nws/translations/pt.json +++ b/homeassistant/components/nws/translations/pt.json @@ -4,7 +4,7 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json index c6b485976a7..43a741977f7 100644 --- a/homeassistant/components/nzbget/translations/ja.json +++ b/homeassistant/components/nzbget/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/nzbget/translations/pt.json b/homeassistant/components/nzbget/translations/pt.json index ba038c72c68..4bbc3a839fd 100644 --- a/homeassistant/components/nzbget/translations/pt.json +++ b/homeassistant/components/nzbget/translations/pt.json @@ -15,7 +15,7 @@ "name": "Nome", "password": "Palavra-passe", "port": "Porta", - "ssl": "NZBGet usa um certificado SSL", + "ssl": "Utiliza um certificado SSL", "username": "Nome de Utilizador", "verify_ssl": "NZBGet usa um certificado adequado" }, diff --git a/homeassistant/components/octoprint/translations/pt.json b/homeassistant/components/octoprint/translations/pt.json new file mode 100644 index 00000000000..7fef03088cb --- /dev/null +++ b/homeassistant/components/octoprint/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "unknown": "Erro inesperado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/ja.json b/homeassistant/components/omnilogic/translations/ja.json index c8f97ac3a40..f66f0a4878f 100644 --- a/homeassistant/components/omnilogic/translations/ja.json +++ b/homeassistant/components/omnilogic/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/oncue/translations/pt.json b/homeassistant/components/oncue/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/oncue/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/pt.json b/homeassistant/components/ondilo_ico/translations/pt.json new file mode 100644 index 00000000000..b7b721c012c --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pt.json b/homeassistant/components/onewire/translations/pt.json index db0e0c2a137..91786f4b324 100644 --- a/homeassistant/components/onewire/translations/pt.json +++ b/homeassistant/components/onewire/translations/pt.json @@ -5,6 +5,13 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/pt.json b/homeassistant/components/onvif/translations/pt.json index f79bbec3201..4240578ca47 100644 --- a/homeassistant/components/onvif/translations/pt.json +++ b/homeassistant/components/onvif/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "O dispositivo ONVIF j\u00e1 est\u00e1 configurado.", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo ONVIF j\u00e1 est\u00e1 em andamento.", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "no_h264": "N\u00e3o existem fluxos H264 dispon\u00edveis. Verifique a configura\u00e7\u00e3o de perfil no seu dispositivo.", "no_mac": "N\u00e3o foi poss\u00edvel configurar o ID unico para o dispositivo ONVIF.", "onvif_error": "Erro ao configurar o dispositivo ONVIF. Verifique os logs para obter mais informa\u00e7\u00f5es." @@ -11,6 +11,11 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "configure": { + "data": { + "host": "Servidor" + } + }, "configure_profile": { "data": { "include": "Criar entidade da c\u00e2mara" diff --git a/homeassistant/components/opengarage/translations/pt.json b/homeassistant/components/opengarage/translations/pt.json new file mode 100644 index 00000000000..1ebfd763b2c --- /dev/null +++ b/homeassistant/components/opengarage/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "port": "Porta", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/pt.json b/homeassistant/components/openuv/translations/pt.json index 6433111fe81..64d5de8785a 100644 --- a/homeassistant/components/openuv/translations/pt.json +++ b/homeassistant/components/openuv/translations/pt.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "Chave de API do OpenUV", + "api_key": "Chave da API", "elevation": "Eleva\u00e7\u00e3o", "latitude": "Latitude", "longitude": "Longitude" diff --git a/homeassistant/components/overkiz/translations/pt.json b/homeassistant/components/overkiz/translations/pt.json new file mode 100644 index 00000000000..1e3d9138c84 --- /dev/null +++ b/homeassistant/components/overkiz/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json index 998478a9cc8..ca30df2b5f0 100644 --- a/homeassistant/components/owntracks/translations/ja.json +++ b/homeassistant/components/owntracks/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\n\nAndroid\u306e\u5834\u5408\u3001[OwnTracks app]({android_url})\u3092\u958b\u304d\u3001\u74b0\u5883\u8a2d\u5b9a -> \u63a5\u7d9a \u306b\u79fb\u52d5\u3057\u3066\u3001\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification(\u8b58\u5225\u60c5\u5831):\n - Username: `''`\n - Device ID: `''`\n\nOS\u306e\u5834\u5408\u3001[OwnTracks app]({ios_url})\u3092\u958b\u304d\u3001\u5de6\u4e0a\u306e(i)\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3057\u3066 -> \u8a2d\u5b9a\u3002\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication(\u8a8d\u8a3c\u3092\u30aa\u30f3\u306b\u3059\u308b)\n - UserID: `''`\n\n{secret}\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/p1_monitor/translations/pt.json b/homeassistant/components/p1_monitor/translations/pt.json new file mode 100644 index 00000000000..38336a1d5de --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/pt.json b/homeassistant/components/philips_js/translations/pt.json index 4646fcae7dc..f6815d0a16a 100644 --- a/homeassistant/components/philips_js/translations/pt.json +++ b/homeassistant/components/philips_js/translations/pt.json @@ -1,13 +1,20 @@ { "config": { "error": { - "invalid_pin": "PIN inv\u00e1lido" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_pin": "PIN inv\u00e1lido", + "unknown": "Erro inesperado" }, "step": { "pair": { "data": { "pin": "C\u00f3digo PIN" } + }, + "user": { + "data": { + "host": "Servidor" + } } } } diff --git a/homeassistant/components/pi_hole/translations/pt.json b/homeassistant/components/pi_hole/translations/pt.json index ce1b6a07d2d..e75629c42e0 100644 --- a/homeassistant/components/pi_hole/translations/pt.json +++ b/homeassistant/components/pi_hole/translations/pt.json @@ -7,6 +7,11 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "api_key": { + "data": { + "api_key": "Chave da API" + } + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/picnic/translations/pt.json b/homeassistant/components/picnic/translations/pt.json new file mode 100644 index 00000000000..79bc83a3f69 --- /dev/null +++ b/homeassistant/components/picnic/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 0842ca6da74..1d7dfc19010 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json index 2eeb965754a..3039abbcafb 100644 --- a/homeassistant/components/plaato/translations/pt.json +++ b/homeassistant/components/plaato/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A Conta j\u00e1 est\u00e1 configurada", + "already_configured": "Conta j\u00e1 configurada", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/plex/translations/pt.json b/homeassistant/components/plex/translations/pt.json index 3b63ab169e2..6daae889e80 100644 --- a/homeassistant/components/plex/translations/pt.json +++ b/homeassistant/components/plex/translations/pt.json @@ -4,7 +4,7 @@ "already_configured": "Este servidor Plex j\u00e1 est\u00e1 configurado", "already_in_progress": "Plex est\u00e1 a ser configurado", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", - "unknown": "Falha por motivo desconhecido" + "unknown": "Erro inesperado" }, "error": { "faulty_credentials": "A autoriza\u00e7\u00e3o falhou, verifique o token" diff --git a/homeassistant/components/point/translations/ja.json b/homeassistant/components/point/translations/ja.json index 6d573895877..884d6d9a1db 100644 --- a/homeassistant/components/point/translations/ja.json +++ b/homeassistant/components/point/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "external_setup": "\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u30dd\u30a4\u30f3\u30c8\u304c\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002", "no_flows": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/point/translations/pt.json b/homeassistant/components/point/translations/pt.json index 3af92508762..fdb8b0c2c8f 100644 --- a/homeassistant/components/point/translations/pt.json +++ b/homeassistant/components/point/translations/pt.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "S\u00f3 pode configurar uma \u00fanica conta Point.", + "already_setup": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/).", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { - "default": "Autenticado com sucesso com Minut para o(s) seu(s) dispositivo (s) Point" + "default": "Autenticado com sucesso" }, "error": { "follow_link": "Por favor, siga o link e autentique antes de pressionar Enviar", diff --git a/homeassistant/components/powerwall/translations/pt.json b/homeassistant/components/powerwall/translations/pt.json index c748619963b..1ded97b1b27 100644 --- a/homeassistant/components/powerwall/translations/pt.json +++ b/homeassistant/components/powerwall/translations/pt.json @@ -8,9 +8,15 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confim": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { - "ip_address": "Endere\u00e7o IP" + "ip_address": "Endere\u00e7o IP", + "password": "Palavra-passe" } } } diff --git a/homeassistant/components/profiler/translations/ja.json b/homeassistant/components/profiler/translations/ja.json index c9c3cc04633..acdaf12a90c 100644 --- a/homeassistant/components/profiler/translations/ja.json +++ b/homeassistant/components/profiler/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/prosegur/translations/pt.json b/homeassistant/components/prosegur/translations/pt.json index d479d880d7f..a4b08e68352 100644 --- a/homeassistant/components/prosegur/translations/pt.json +++ b/homeassistant/components/prosegur/translations/pt.json @@ -8,6 +8,11 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/ps4/translations/pt.json b/homeassistant/components/ps4/translations/pt.json index cf428838b0b..ac585a23994 100644 --- a/homeassistant/components/ps4/translations/pt.json +++ b/homeassistant/components/ps4/translations/pt.json @@ -15,7 +15,7 @@ "step": { "link": { "data": { - "code": "PIN", + "code": "C\u00f3digo PIN", "ip_address": "Endere\u00e7o de IP", "name": "Nome", "region": "Regi\u00e3o" diff --git a/homeassistant/components/pure_energie/translations/pt.json b/homeassistant/components/pure_energie/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvoutput/translations/pt.json b/homeassistant/components/pvoutput/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/pvoutput/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/pt.json b/homeassistant/components/qnap_qsw/translations/pt.json new file mode 100644 index 00000000000..9f02a9e2519 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "discovered_connection": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/ja.json b/homeassistant/components/radio_browser/translations/ja.json index 24b32e6e30a..0143961d6ce 100644 --- a/homeassistant/components/radio_browser/translations/ja.json +++ b/homeassistant/components/radio_browser/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/radio_browser/translations/pt.json b/homeassistant/components/radio_browser/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/pt.json b/homeassistant/components/radiotherm/translations/pt.json new file mode 100644 index 00000000000..ae100e45845 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/pt.json b/homeassistant/components/rainforest_eagle/translations/pt.json index 4e8578a0a28..91786f4b324 100644 --- a/homeassistant/components/rainforest_eagle/translations/pt.json +++ b/homeassistant/components/rainforest_eagle/translations/pt.json @@ -1,9 +1,15 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { - "host": "Anfitri\u00e3o" + "host": "Servidor" } } } diff --git a/homeassistant/components/rdw/translations/pt.json b/homeassistant/components/rdw/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/rdw/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/pt.json b/homeassistant/components/renault/translations/pt.json new file mode 100644 index 00000000000..f2223dc3827 --- /dev/null +++ b/homeassistant/components/renault/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_credentials": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json index 9b22d34af58..755621839c8 100644 --- a/homeassistant/components/rfxtrx/translations/ja.json +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/rhasspy/translations/ca.json b/homeassistant/components/rhasspy/translations/ca.json new file mode 100644 index 00000000000..cc92e3ec9f1 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/ca.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/de.json b/homeassistant/components/rhasspy/translations/de.json new file mode 100644 index 00000000000..953cb89400a --- /dev/null +++ b/homeassistant/components/rhasspy/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest du die Rhasspy-Unterst\u00fctzung aktivieren?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/el.json b/homeassistant/components/rhasspy/translations/el.json new file mode 100644 index 00000000000..fe181ecdb04 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/el.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 Rhasspy;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/ja.json b/homeassistant/components/rhasspy/translations/ja.json new file mode 100644 index 00000000000..d6f1b06b228 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + }, + "step": { + "user": { + "description": "Rhasspy\u306e\u30b5\u30dd\u30fc\u30c8\u3092\u6709\u52b9\u306b\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/pt.json b/homeassistant/components/rhasspy/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/zh-Hant.json b/homeassistant/components/rhasspy/translations/zh-Hant.json new file mode 100644 index 00000000000..ce30b5189a6 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u555f Rhasspy \u652f\u63f4\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/pt.json b/homeassistant/components/ridwell/translations/pt.json new file mode 100644 index 00000000000..3d5061419fc --- /dev/null +++ b/homeassistant/components/ridwell/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json index 3afb1cc22e3..5e40221cec6 100644 --- a/homeassistant/components/roomba/translations/pt.json +++ b/homeassistant/components/roomba/translations/pt.json @@ -4,13 +4,18 @@ "not_irobot_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo iRobot" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente" + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "flow_title": "iRobot {name} ({host})", "step": { "link": { "title": "Recuperar Palavra-passe" }, + "link_manual": { + "data": { + "password": "Palavra-passe" + } + }, "manual": { "data": { "host": "Anfitri\u00e3o" diff --git a/homeassistant/components/roon/translations/pt.json b/homeassistant/components/roon/translations/pt.json index 1e12fdcfcba..105f0b64676 100644 --- a/homeassistant/components/roon/translations/pt.json +++ b/homeassistant/components/roon/translations/pt.json @@ -6,6 +6,13 @@ "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "fallback": { + "data": { + "host": "Servidor" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/ja.json b/homeassistant/components/rpi_power/translations/ja.json index 26aee66af5b..2c2dff7e51e 100644 --- a/homeassistant/components/rpi_power/translations/ja.json +++ b/homeassistant/components/rpi_power/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u3053\u306e\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306b\u5fc5\u8981\u306a\u30b7\u30b9\u30c6\u30e0\u30af\u30e9\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u30ab\u30fc\u30cd\u30eb\u304c\u6700\u65b0\u3067\u3001\u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/ja.json b/homeassistant/components/rtsp_to_webrtc/translations/ja.json index c904164a599..2f6c9eb0f80 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/ja.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "server_failure": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u304c\u30a8\u30e9\u30fc\u3092\u8fd4\u3057\u307e\u3057\u305f\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "server_unreachable": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u3068\u306e\u901a\u4fe1\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_url": "\u6709\u52b9\u306aRTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4f8b: https://example.com", diff --git a/homeassistant/components/rtsp_to_webrtc/translations/pt.json b/homeassistant/components/rtsp_to_webrtc/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/rtsp_to_webrtc/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/pt.json b/homeassistant/components/sabnzbd/translations/pt.json new file mode 100644 index 00000000000..20bba0ede4b --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index b2cd242c7da..c0c3fb735c1 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -5,7 +5,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, - "flow_title": "TV Samsung: {model}", + "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/scrape/translations/pt.json b/homeassistant/components/scrape/translations/pt.json new file mode 100644 index 00000000000..00c65c09cdf --- /dev/null +++ b/homeassistant/components/scrape/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/pt.json b/homeassistant/components/screenlogic/translations/pt.json new file mode 100644 index 00000000000..218e55c6d5f --- /dev/null +++ b/homeassistant/components/screenlogic/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "gateway_entry": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/pt.json b/homeassistant/components/sense/translations/pt.json index e3b78cd8e42..d429d0af1e8 100644 --- a/homeassistant/components/sense/translations/pt.json +++ b/homeassistant/components/sense/translations/pt.json @@ -9,6 +9,9 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_validate": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/senseme/translations/pt.json b/homeassistant/components/senseme/translations/pt.json new file mode 100644 index 00000000000..4c3266a6022 --- /dev/null +++ b/homeassistant/components/senseme/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + }, + "step": { + "manual": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/pt.json b/homeassistant/components/sensibo/translations/pt.json new file mode 100644 index 00000000000..80f65d0a06d --- /dev/null +++ b/homeassistant/components/sensibo/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/ja.json b/homeassistant/components/sentry/translations/ja.json index 8ac8ebf58a1..56312cecda3 100644 --- a/homeassistant/components/sentry/translations/ja.json +++ b/homeassistant/components/sentry/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "bad_dsn": "\u7121\u52b9\u306aDSN", diff --git a/homeassistant/components/shopping_list/translations/pt.json b/homeassistant/components/shopping_list/translations/pt.json index 9e8b24efa29..bdb2d4041ef 100644 --- a/homeassistant/components/shopping_list/translations/pt.json +++ b/homeassistant/components/shopping_list/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A lista de compras j\u00e1 est\u00e1 configurada." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "step": { "user": { diff --git a/homeassistant/components/sia/translations/pt.json b/homeassistant/components/sia/translations/pt.json new file mode 100644 index 00000000000..0077ceddd46 --- /dev/null +++ b/homeassistant/components/sia/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pt.json b/homeassistant/components/simplepush/translations/pt.json new file mode 100644 index 00000000000..d7e598b33e4 --- /dev/null +++ b/homeassistant/components/simplepush/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/pt.json b/homeassistant/components/skybell/translations/pt.json new file mode 100644 index 00000000000..8487c8869b6 --- /dev/null +++ b/homeassistant/components/skybell/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/pt.json b/homeassistant/components/slack/translations/pt.json new file mode 100644 index 00000000000..cb9139874f2 --- /dev/null +++ b/homeassistant/components/slack/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/pt.json b/homeassistant/components/sleepiq/translations/pt.json new file mode 100644 index 00000000000..5602f898fc3 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/ja.json b/homeassistant/components/slimproto/translations/ja.json index 4b8d0691b68..cf3ac93acad 100644 --- a/homeassistant/components/slimproto/translations/ja.json +++ b/homeassistant/components/slimproto/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" } } } \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/pt.json b/homeassistant/components/slimproto/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/slimproto/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/pt.json b/homeassistant/components/smartthings/translations/pt.json index 9f2ed5a4b90..a6297502b0e 100644 --- a/homeassistant/components/smartthings/translations/pt.json +++ b/homeassistant/components/smartthings/translations/pt.json @@ -28,7 +28,7 @@ "title": "Selecionar Localiza\u00e7\u00e3o" }, "user": { - "description": "Por favor, insira um SmartThings [Personal Access Token]({token_url} ) que foi criado de acordo com as [instru\u00e7\u00f5es]({component_url}).", + "description": "SmartThings will be configured to send push updates to Home Assistant at:\n> {webhook_url}\n\nIf this is not correct, please update your configuration, restart Home Assistant, and try again.", "title": "Insira o Token de acesso pessoal" } } diff --git a/homeassistant/components/smarttub/translations/pt.json b/homeassistant/components/smarttub/translations/pt.json index 5d4e3e1faed..c6325f3dba3 100644 --- a/homeassistant/components/smarttub/translations/pt.json +++ b/homeassistant/components/smarttub/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Conta j\u00e1 configurada", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { diff --git a/homeassistant/components/smhi/translations/pt.json b/homeassistant/components/smhi/translations/pt.json index d5cd5e83a13..acdfb6606bd 100644 --- a/homeassistant/components/smhi/translations/pt.json +++ b/homeassistant/components/smhi/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia" }, diff --git a/homeassistant/components/sms/translations/ja.json b/homeassistant/components/sms/translations/ja.json index ddfb644d90f..88780e7aef0 100644 --- a/homeassistant/components/sms/translations/ja.json +++ b/homeassistant/components/sms/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/solax/translations/pt.json b/homeassistant/components/solax/translations/pt.json new file mode 100644 index 00000000000..916bd3daced --- /dev/null +++ b/homeassistant/components/solax/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/ja.json b/homeassistant/components/soma/translations/ja.json index 026499458d8..d3415c1d197 100644 --- a/homeassistant/components/soma/translations/ja.json +++ b/homeassistant/components/soma/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "missing_configuration": "SOMA Connect\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/soma/translations/pt.json b/homeassistant/components/soma/translations/pt.json index f681da4210f..9199c825f4a 100644 --- a/homeassistant/components/soma/translations/pt.json +++ b/homeassistant/components/soma/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_setup": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/pt.json b/homeassistant/components/somfy_mylink/translations/pt.json new file mode 100644 index 00000000000..fe98366e28e --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "" + }, + "options": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/ja.json b/homeassistant/components/sonos/translations/ja.json index 7aa5823c7c6..d4b696cbb48 100644 --- a/homeassistant/components/sonos/translations/ja.json +++ b/homeassistant/components/sonos/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "not_sonos_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Sonos\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/translations/pt.json b/homeassistant/components/sonos/translations/pt.json index 51fbd16a20d..5b0b34a8eb7 100644 --- a/homeassistant/components/sonos/translations/pt.json +++ b/homeassistant/components/sonos/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo Sonos encontrado na rede.", + "no_devices_found": "Nenhum dispositivo encontrado na rede", "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Sonos \u00e9 necess\u00e1ria." }, "step": { diff --git a/homeassistant/components/soundtouch/translations/pt.json b/homeassistant/components/soundtouch/translations/pt.json new file mode 100644 index 00000000000..91786f4b324 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index 5712139b6b9..40f592b2c46 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/spider/translations/ja.json b/homeassistant/components/spider/translations/ja.json index 9277adceeee..f45ec1ebb98 100644 --- a/homeassistant/components/spider/translations/ja.json +++ b/homeassistant/components/spider/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json index 805a500502b..2e33e78e8d8 100644 --- a/homeassistant/components/srp_energy/translations/ja.json +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/steam_online/translations/pt.json b/homeassistant/components/steam_online/translations/pt.json new file mode 100644 index 00000000000..18ac9fb4b40 --- /dev/null +++ b/homeassistant/components/steam_online/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steamist/translations/pt.json b/homeassistant/components/steamist/translations/pt.json new file mode 100644 index 00000000000..6ead5b65917 --- /dev/null +++ b/homeassistant/components/steamist/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sun/translations/ja.json b/homeassistant/components/sun/translations/ja.json index 8188e950389..ecb23ee53d6 100644 --- a/homeassistant/components/sun/translations/ja.json +++ b/homeassistant/components/sun/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/sun/translations/pt.json b/homeassistant/components/sun/translations/pt.json index 2f060112a0c..d050eb7c5f5 100644 --- a/homeassistant/components/sun/translations/pt.json +++ b/homeassistant/components/sun/translations/pt.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + }, "state": { "_": { "above_horizon": "Acima do horizonte", diff --git a/homeassistant/components/surepetcare/translations/pt.json b/homeassistant/components/surepetcare/translations/pt.json new file mode 100644 index 00000000000..0e89c76d047 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/pt.json b/homeassistant/components/switchbot/translations/pt.json new file mode 100644 index 00000000000..b8a454fbaba --- /dev/null +++ b/homeassistant/components/switchbot/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/ja.json b/homeassistant/components/switcher_kis/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/switcher_kis/translations/ja.json +++ b/homeassistant/components/switcher_kis/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/syncthing/translations/pt.json b/homeassistant/components/syncthing/translations/pt.json new file mode 100644 index 00000000000..51baddfeab3 --- /dev/null +++ b/homeassistant/components/syncthing/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/pt.json b/homeassistant/components/synology_dsm/translations/pt.json index 9745f897e05..66df18026ea 100644 --- a/homeassistant/components/synology_dsm/translations/pt.json +++ b/homeassistant/components/synology_dsm/translations/pt.json @@ -23,6 +23,12 @@ "verify_ssl": "Verificar o certificado SSL" } }, + "reauth_confirm": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/system_bridge/translations/pt.json b/homeassistant/components/system_bridge/translations/pt.json new file mode 100644 index 00000000000..8f319572c97 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/pt.json b/homeassistant/components/tankerkoenig/translations/pt.json index 19646bbce9c..7af02efc468 100644 --- a/homeassistant/components/tankerkoenig/translations/pt.json +++ b/homeassistant/components/tankerkoenig/translations/pt.json @@ -1,6 +1,14 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, "user": { "data": { "name": "Nome da regi\u00e3o" diff --git a/homeassistant/components/tasmota/translations/ja.json b/homeassistant/components/tasmota/translations/ja.json index 8aad41de32f..34c56d4d413 100644 --- a/homeassistant/components/tasmota/translations/ja.json +++ b/homeassistant/components/tasmota/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_discovery_topic": "(\u4e0d\u6b63\u306a)Invalid discovery topic prefix." diff --git a/homeassistant/components/tautulli/translations/ja.json b/homeassistant/components/tautulli/translations/ja.json index 6f733e1cad4..2407bb4b984 100644 --- a/homeassistant/components/tautulli/translations/ja.json +++ b/homeassistant/components/tautulli/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/tautulli/translations/pt.json b/homeassistant/components/tautulli/translations/pt.json new file mode 100644 index 00000000000..43d522f0ab1 --- /dev/null +++ b/homeassistant/components/tautulli/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/pt.json b/homeassistant/components/tellduslive/translations/pt.json index 1f06d33d356..e8fda8a7647 100644 --- a/homeassistant/components/tellduslive/translations/pt.json +++ b/homeassistant/components/tellduslive/translations/pt.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", - "unknown": "Ocorreu um erro desconhecido", + "unknown": "Erro inesperado", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "error": { diff --git a/homeassistant/components/tesla_wall_connector/translations/pt.json b/homeassistant/components/tesla_wall_connector/translations/pt.json new file mode 100644 index 00000000000..8578f969852 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/pt.json b/homeassistant/components/tile/translations/pt.json index bfafaa77b42..e883bd875e1 100644 --- a/homeassistant/components/tile/translations/pt.json +++ b/homeassistant/components/tile/translations/pt.json @@ -7,10 +7,15 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", - "username": "E-mail" + "username": "Email" }, "title": "Configurar Tile" } diff --git a/homeassistant/components/tomorrowio/translations/pt.json b/homeassistant/components/tomorrowio/translations/pt.json new file mode 100644 index 00000000000..1e8c651e5e8 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "location": "Localiza\u00e7\u00e3o", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/pt.json b/homeassistant/components/tplink/translations/pt.json index 90afdfcf10a..d4507db3a55 100644 --- a/homeassistant/components/tplink/translations/pt.json +++ b/homeassistant/components/tplink/translations/pt.json @@ -2,6 +2,9 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo TP-Link encontrado na rede." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/ja.json b/homeassistant/components/traccar/translations/ja.json index c635e23fbe0..73c7de74ced 100644 --- a/homeassistant/components/traccar/translations/ja.json +++ b/homeassistant/components/traccar/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/trafikverket_ferry/translations/pt.json b/homeassistant/components/trafikverket_ferry/translations/pt.json new file mode 100644 index 00000000000..8cd0d6c0842 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/pt.json b/homeassistant/components/trafikverket_weatherstation/translations/pt.json new file mode 100644 index 00000000000..8cd0d6c0842 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/pt.json b/homeassistant/components/transmission/translations/pt.json index c3d4131d995..399596eff8b 100644 --- a/homeassistant/components/transmission/translations/pt.json +++ b/homeassistant/components/transmission/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", @@ -9,6 +10,12 @@ "name_exists": "Nome j\u00e1 existe" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/tuya/translations/select.pt.json b/homeassistant/components/tuya/translations/select.pt.json index 77604504f4d..fac54570cd6 100644 --- a/homeassistant/components/tuya/translations/select.pt.json +++ b/homeassistant/components/tuya/translations/select.pt.json @@ -1,10 +1,18 @@ { "state": { + "tuya__basic_nightvision": { + "1": "Desligado" + }, + "tuya__light_mode": { + "none": "Desligado" + }, "tuya__record_mode": { "2": "Grava\u00e7\u00e3o cont\u00ednua" }, "tuya__relay_status": { - "on": "Ligado" + "off": "Desligado", + "on": "Ligado", + "power_off": "Desligado" }, "tuya__vacuum_mode": { "pick_zone": "Escolha a Zona", diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json index 521fee184f2..4ebff4fde6e 100644 --- a/homeassistant/components/twilio/translations/ja.json +++ b/homeassistant/components/twilio/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/unifi/translations/pt.json b/homeassistant/components/unifi/translations/pt.json index 7a0a8e1a1fd..b62d5f4104e 100644 --- a/homeassistant/components/unifi/translations/pt.json +++ b/homeassistant/components/unifi/translations/pt.json @@ -15,7 +15,7 @@ "port": "Porto", "site": "Site ID", "username": "Nome do utilizador", - "verify_ssl": "Controlador com certificados adequados" + "verify_ssl": "Verificar o certificado SSL" }, "title": "Configurar o controlador UniFi" } diff --git a/homeassistant/components/unifiprotect/translations/pt.json b/homeassistant/components/unifiprotect/translations/pt.json new file mode 100644 index 00000000000..253a0f414df --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "discovery_confirm": { + "data": { + "username": "Nome de Utilizador" + }, + "description": "Do you want to setup {name} ({ip_address})? " + }, + "reauth_confirm": { + "data": { + "port": "Porta" + } + }, + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "port": "Porta", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/pt.json b/homeassistant/components/upnp/translations/pt.json index 022d1c823c1..6fa823925e1 100644 --- a/homeassistant/components/upnp/translations/pt.json +++ b/homeassistant/components/upnp/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "UPnP/IGD j\u00e1 est\u00e1 configurado", - "no_devices_found": "Nenhum dispositivo UPnP / IGD encontrado na rede." + "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "error": { "one": "um", diff --git a/homeassistant/components/uptime/translations/ja.json b/homeassistant/components/uptime/translations/ja.json index 99dc644a0e5..8614c751069 100644 --- a/homeassistant/components/uptime/translations/ja.json +++ b/homeassistant/components/uptime/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/uptimerobot/translations/pt.json b/homeassistant/components/uptimerobot/translations/pt.json index 10c16aafa0f..826cdc5aa59 100644 --- a/homeassistant/components/uptimerobot/translations/pt.json +++ b/homeassistant/components/uptimerobot/translations/pt.json @@ -4,7 +4,20 @@ "unknown": "Erro inesperado" }, "error": { - "invalid_api_key": "Chave de API inv\u00e1lida" + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "api_key": "Chave da API" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/pt.json b/homeassistant/components/vallox/translations/pt.json index 0c5c7760566..f5c3b1c7b61 100644 --- a/homeassistant/components/vallox/translations/pt.json +++ b/homeassistant/components/vallox/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + }, "error": { "unknown": "Erro inesperado" } diff --git a/homeassistant/components/venstar/translations/pt.json b/homeassistant/components/venstar/translations/pt.json new file mode 100644 index 00000000000..04374af8e82 --- /dev/null +++ b/homeassistant/components/venstar/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/ja.json b/homeassistant/components/vesync/translations/ja.json index 66d7cf26ca9..c6717072946 100644 --- a/homeassistant/components/vesync/translations/ja.json +++ b/homeassistant/components/vesync/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" diff --git a/homeassistant/components/vicare/translations/ja.json b/homeassistant/components/vicare/translations/ja.json index 10ffe077aca..8d50ef180d1 100644 --- a/homeassistant/components/vicare/translations/ja.json +++ b/homeassistant/components/vicare/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/vicare/translations/pt.json b/homeassistant/components/vicare/translations/pt.json new file mode 100644 index 00000000000..6a34ef9530f --- /dev/null +++ b/homeassistant/components/vicare/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "client_id": "Chave da API", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/pt.json b/homeassistant/components/vlc_telnet/translations/pt.json new file mode 100644 index 00000000000..55ccd56b497 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/pt.json b/homeassistant/components/wallbox/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/wallbox/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/pt.json b/homeassistant/components/watttime/translations/pt.json index d64652ed815..859d8de1627 100644 --- a/homeassistant/components/watttime/translations/pt.json +++ b/homeassistant/components/watttime/translations/pt.json @@ -1,9 +1,12 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { - "username": "Nome de utilizador" + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/waze_travel_time/translations/pt.json b/homeassistant/components/waze_travel_time/translations/pt.json new file mode 100644 index 00000000000..3d986f4533d --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/translations/pt.json b/homeassistant/components/weather/translations/pt.json index 5875b8a7192..13c5273632c 100644 --- a/homeassistant/components/weather/translations/pt.json +++ b/homeassistant/components/weather/translations/pt.json @@ -1,13 +1,13 @@ { "state": { "_": { - "clear-night": "Limpo, Noite", + "clear-night": "C\u00e9u limpo, Noite", "cloudy": "Nublado", "exceptional": "Excecional", "fog": "Nevoeiro", "hail": "Granizo", "lightning": "Rel\u00e2mpago", - "lightning-rainy": "Rel\u00e2mpagos, chuva", + "lightning-rainy": "Trovoada, chuva", "partlycloudy": "Parcialmente nublado", "pouring": "Chuva forte", "rainy": "Chuva", diff --git a/homeassistant/components/webostv/translations/pt.json b/homeassistant/components/webostv/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/webostv/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/ja.json b/homeassistant/components/wemo/translations/ja.json index f86e1e80520..7ab4a0bfc1b 100644 --- a/homeassistant/components/wemo/translations/ja.json +++ b/homeassistant/components/wemo/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/whirlpool/translations/pt.json b/homeassistant/components/whirlpool/translations/pt.json new file mode 100644 index 00000000000..ce1bf4bb4b8 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whois/translations/pt.json b/homeassistant/components/whois/translations/pt.json new file mode 100644 index 00000000000..d252c078a2c --- /dev/null +++ b/homeassistant/components/whois/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/el.json b/homeassistant/components/withings/translations/el.json index dc284985ee2..068d347467a 100644 --- a/homeassistant/components/withings/translations/el.json +++ b/homeassistant/components/withings/translations/el.json @@ -27,6 +27,10 @@ "reauth": { "description": "\u03a4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \"{profile}\" \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 Withings.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "reauth_confirm": { + "description": "\u03a4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \"{profile}\" \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 Withings.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } } diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index 20f097e973c..3fdcffdb918 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -27,6 +27,10 @@ "reauth": { "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, + "reauth_confirm": { + "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/withings/translations/pt.json b/homeassistant/components/withings/translations/pt.json index 1fe7083ecfd..673627c8036 100644 --- a/homeassistant/components/withings/translations/pt.json +++ b/homeassistant/components/withings/translations/pt.json @@ -19,6 +19,9 @@ }, "reauth": { "title": "Re-autenticar Perfil" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" } } } diff --git a/homeassistant/components/wiz/translations/pt.json b/homeassistant/components/wiz/translations/pt.json new file mode 100644 index 00000000000..b686fee56b5 --- /dev/null +++ b/homeassistant/components/wiz/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.pt.json b/homeassistant/components/wled/translations/select.pt.json new file mode 100644 index 00000000000..d33123843d4 --- /dev/null +++ b/homeassistant/components/wled/translations/select.pt.json @@ -0,0 +1,8 @@ +{ + "state": { + "wled__live_override": { + "0": "Desligado", + "1": "Ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/pt.json b/homeassistant/components/ws66i/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/ws66i/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index 7a9337c9332..2d1e95019b7 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/xiaomi_miio/translations/pt.json b/homeassistant/components/xiaomi_miio/translations/pt.json index db0e0c2a137..41cb2e55e91 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt.json +++ b/homeassistant/components/xiaomi_miio/translations/pt.json @@ -5,6 +5,14 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "manual": { + "data": { + "host": "Endere\u00e7o IP", + "token": "API Token" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/pt.json b/homeassistant/components/yale_smart_alarm/translations/pt.json new file mode 100644 index 00000000000..0b1a6c82cf3 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "name": "Nome", + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/pt.json b/homeassistant/components/yolink/translations/pt.json new file mode 100644 index 00000000000..1ffa7cb5245 --- /dev/null +++ b/homeassistant/components/yolink/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/pt.json b/homeassistant/components/youless/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/youless/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/ja.json b/homeassistant/components/zerproc/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/zerproc/translations/ja.json +++ b/homeassistant/components/zerproc/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 636df6f047e..9e8289960f8 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_zha_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fzha\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "usb_probe_failed": "USB\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u51fa\u3059\u3053\u3068\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index f9d67cc2d35..be7fb30dbec 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -40,14 +40,14 @@ }, "config_panel": { "zha_alarm_options": { - "alarm_arm_requires_code": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "alarm_arm_requires_code": "\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", "alarm_failed_tries": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043d\u0435\u0443\u0434\u0430\u0447\u043d\u044b\u0445 \u0432\u0432\u043e\u0434\u043e\u0432 \u043a\u043e\u0434\u0430, \u0434\u043b\u044f \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0442\u0440\u0435\u0432\u043e\u0433\u0438", "alarm_master_code": "\u041c\u0430\u0441\u0442\u0435\u0440-\u043a\u043e\u0434 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u043d\u0435\u043b\u0435\u0439", - "title": "\u041e\u043f\u0446\u0438\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u0435\u0439 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" }, "zha_options": { - "consider_unavailable_battery": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0441\u0435\u043a\u0443\u043d\u0434)", - "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0441\u0435\u043a\u0443\u043d\u0434)", + "consider_unavailable_battery": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" diff --git a/homeassistant/components/zwave_js/translations/pt.json b/homeassistant/components/zwave_js/translations/pt.json new file mode 100644 index 00000000000..086d4c19ebb --- /dev/null +++ b/homeassistant/components/zwave_js/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + }, + "options": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "manual": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/pt.json b/homeassistant/components/zwave_me/translations/pt.json new file mode 100644 index 00000000000..a142354a462 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "token": "API Token", + "url": "" + } + } + } + } +} \ No newline at end of file From 3ca04aa33b80759acd9f2a660b1035adab184eb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Jul 2022 08:40:43 +0200 Subject: [PATCH 409/820] Bump actions/cache from 3.0.4 to 3.0.5 (#75104) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a5f3da97ac4..573445a4d2e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: cache: "pip" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -185,7 +185,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -211,7 +211,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -222,7 +222,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -260,7 +260,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -271,7 +271,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -312,7 +312,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -323,7 +323,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -353,7 +353,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -364,7 +364,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -480,7 +480,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -488,7 +488,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PIP_CACHE }} key: >- @@ -538,7 +538,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -570,7 +570,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -603,7 +603,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -647,7 +647,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -695,7 +695,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -749,7 +749,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From ad82352daeb280c14738627bf577195cd8dfcf71 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 13 Jul 2022 02:46:32 -0400 Subject: [PATCH 410/820] Bump AIOAladdinConnect to 0.1.23 (#75065) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 3a9e295a08f..bdf906ad200 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.21"], + "requirements": ["AIOAladdinConnect==0.1.23"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 36b3ba5cb62..ca2a76ec5f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.21 +AIOAladdinConnect==0.1.23 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4e8b4262f6..cfda178b48a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.21 +AIOAladdinConnect==0.1.23 # homeassistant.components.adax Adax-local==0.1.4 From 1e5ada0f9db8a5e95c6cd2a4229c7f9be59716be Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 13 Jul 2022 02:56:08 -0400 Subject: [PATCH 411/820] Fix Insteon thermostat issues (#75079) --- homeassistant/components/insteon/climate.py | 2 +- homeassistant/components/insteon/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 97e21a02f6f..29c127ba0c7 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -86,7 +86,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self._insteon_device.properties[CELSIUS].value: + if self._insteon_device.configuration[CELSIUS].value: return TEMP_CELSIUS return TEMP_FAHRENHEIT diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 1be077a6b38..c48d502c16e 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,8 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.1", - "insteon-frontend-home-assistant==0.1.1" + "pyinsteon==1.1.3", + "insteon-frontend-home-assistant==0.2.0" ], "codeowners": ["@teharris1"], "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index ca2a76ec5f1..d84fa688c87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -894,7 +894,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.1 +insteon-frontend-home-assistant==0.2.0 # homeassistant.components.intellifire intellifire4py==2.0.1 @@ -1562,7 +1562,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.1 +pyinsteon==1.1.3 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfda178b48a..57b73ced72e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -640,7 +640,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.1 +insteon-frontend-home-assistant==0.2.0 # homeassistant.components.intellifire intellifire4py==2.0.1 @@ -1059,7 +1059,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.1 +pyinsteon==1.1.3 # homeassistant.components.ipma pyipma==2.0.5 From b7a6f4e22036dfdde69fede8840ecc41d9398c4d Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 13 Jul 2022 10:12:50 +0300 Subject: [PATCH 412/820] Fix missing ordered states in universal media player (#75099) --- homeassistant/components/universal/media_player.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index f33db3827af..352703f8f9b 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -69,11 +69,13 @@ from homeassistant.const import ( SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + STATE_BUFFERING, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING, + STATE_STANDBY, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -101,8 +103,10 @@ STATES_ORDER = [ STATE_UNAVAILABLE, STATE_OFF, STATE_IDLE, + STATE_STANDBY, STATE_ON, STATE_PAUSED, + STATE_BUFFERING, STATE_PLAYING, ] ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) From 34f1d5e094745e0331463904b5fbc49761234dea Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 13 Jul 2022 09:21:58 +0200 Subject: [PATCH 413/820] Add Plugwise number platform (#74655) --- homeassistant/components/plugwise/const.py | 3 +- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 115 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/anna_heatpump/all_data.json | 3 + tests/components/plugwise/test_number.py | 40 ++++++ 7 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/plugwise/number.py create mode 100644 tests/components/plugwise/test_number.py diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 63bd2a6d8f1..d56d9c06ff5 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -25,8 +25,9 @@ UNIT_LUMEN: Final = "lm" PLATFORMS_GATEWAY: Final[list[str]] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, - Platform.SENSOR, + Platform.NUMBER, Platform.SELECT, + Platform.SENSOR, Platform.SWITCH, ] ZEROCONF_MAP: Final[dict[str, str]] = { diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index a311151f645..f0e7f98d4b8 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.18.5"], + "requirements": ["plugwise==0.18.6"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py new file mode 100644 index 00000000000..6718c59c3c0 --- /dev/null +++ b/homeassistant/components/plugwise/number.py @@ -0,0 +1,115 @@ +"""Number platform for Plugwise integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from plugwise import Smile + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity + + +@dataclass +class PlugwiseEntityDescriptionMixin: + """Mixin values for Plugwse entities.""" + + command: Callable[[Smile, float], Awaitable[None]] + + +@dataclass +class PlugwiseNumberEntityDescription( + NumberEntityDescription, PlugwiseEntityDescriptionMixin +): + """Class describing Plugwise Number entities.""" + + +NUMBER_TYPES = ( + PlugwiseNumberEntityDescription( + key="maximum_boiler_temperature", + command=lambda api, value: api.set_max_boiler_temperature(value), + device_class=NumberDeviceClass.TEMPERATURE, + name="Maximum Boiler Temperature Setpoint", + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=TEMP_CELSIUS, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Plugwise number platform.""" + + coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + entities: list[PlugwiseNumberEntity] = [] + for device_id, device in coordinator.data.devices.items(): + for description in NUMBER_TYPES: + if description.key in device: + entities.append( + PlugwiseNumberEntity(coordinator, device_id, description) + ) + + async_add_entities(entities) + + +class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): + """Representation of a Plugwise number.""" + + entity_description: PlugwiseNumberEntityDescription + + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + description: PlugwiseNumberEntityDescription, + ) -> None: + """Initiate Plugwise Number.""" + super().__init__(coordinator, device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}-{description.key}" + self._attr_name = (f"{self.device['name']} {description.name}").lstrip() + self._attr_mode = NumberMode.BOX + + @property + def native_step(self) -> float: + """Return the setpoint step value.""" + return max(self.device["resolution"], 1) + + @property + def native_value(self) -> float: + """Return the present setpoint value.""" + return self.device[self.entity_description.key] + + @property + def native_min_value(self) -> float: + """Return the setpoint min. value.""" + return self.device["lower_bound"] + + @property + def native_max_value(self) -> float: + """Return the setpoint max. value.""" + return self.device["upper_bound"] + + async def async_set_native_value(self, value: float) -> None: + """Change to the new setpoint value.""" + await self.entity_description.command(self.coordinator.api, value) + await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index d84fa688c87..5dd7d0f871f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1254,7 +1254,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.5 +plugwise==0.18.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57b73ced72e..d3bb3335510 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -862,7 +862,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.5 +plugwise==0.18.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json index 60bc4c35668..6fcb841cf3e 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json @@ -13,6 +13,9 @@ "model": "Generic heater", "name": "OpenTherm", "vendor": "Techneco", + "lower_bound": 0.0, + "upper_bound": 100.0, + "resolution": 1.0, "maximum_boiler_temperature": 60.0, "binary_sensors": { "dhw_state": false, diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py new file mode 100644 index 00000000000..a4e084e5d3a --- /dev/null +++ b/tests/components/plugwise/test_number.py @@ -0,0 +1,40 @@ +"""Tests for the Plugwise Number integration.""" + +from unittest.mock import MagicMock + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_anna_number_entities( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of a number.""" + state = hass.states.get("number.opentherm_maximum_boiler_temperature_setpoint") + assert state + assert float(state.state) == 60.0 + + +async def test_anna_max_boiler_temp_change( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test changing of number entities.""" + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.opentherm_maximum_boiler_temperature_setpoint", + ATTR_VALUE: 65, + }, + blocking=True, + ) + + assert mock_smile_anna.set_max_boiler_temperature.call_count == 1 + mock_smile_anna.set_max_boiler_temperature.assert_called_with(65) From 2169d839ce24c6e2b9fa215c5cabbd48b8a4022f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Jul 2022 03:08:23 -0600 Subject: [PATCH 414/820] Remove service descriptions for deprecated Guardian services (#75084) --- .../components/guardian/services.yaml | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index 4d48783c955..61cf709a31c 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -39,28 +39,6 @@ pair_sensor: example: 5410EC688BCF selector: text: -reboot: - name: Reboot - description: Reboot the device. - fields: - device_id: - name: Valve Controller - description: The valve controller to reboot - required: true - selector: - device: - integration: guardian -reset_valve_diagnostics: - name: Reset Valve Diagnostics - description: Fully (and irrecoverably) reset all valve diagnostics. - fields: - device_id: - name: Valve Controller - description: The valve controller whose diagnostics should be reset - required: true - selector: - device: - integration: guardian unpair_sensor: name: Unpair Sensor description: Remove a paired sensor from the valve controller. From 4a36318d56673b22f56630eb04919463444aec19 Mon Sep 17 00:00:00 2001 From: Everything Smart Home <53482654+EverythingSmartHome@users.noreply.github.com> Date: Wed, 13 Jul 2022 18:58:36 +0100 Subject: [PATCH 415/820] Add Aqara E1 curtain motor direction select entity to ZHA (#75132) * Add direction change select option for Aqara Curtain * Add direction change select option for Aqara Curtain --- homeassistant/components/zha/select.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 79349273e38..e2835d4acd4 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -264,3 +264,20 @@ class AqaraApproachDistance(ZCLEnumSelectEntity, id_suffix="approach_distance"): _select_attr = "approach_distance" _enum = AqaraApproachDistances + + +class AqaraE1ReverseDirection(types.enum8): + """Aqara curtain reversal.""" + + Normal = 0x00 + Inverted = 0x01 + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="window_covering", models={"lumi.curtain.agl001"} +) +class AqaraCurtainMode(ZCLEnumSelectEntity, id_suffix="window_covering_mode"): + """Representation of a ZHA curtain mode configuration entity.""" + + _select_attr = "window_covering_mode" + _enum = AqaraE1ReverseDirection From ffeac9714f69ebeb4248fa38e6ac6c8e6cf12a5e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Jul 2022 12:52:13 -0600 Subject: [PATCH 416/820] Ensure SimpliSafe diagnostics redact the `code` option (#75137) --- homeassistant/components/simplisafe/diagnostics.py | 3 ++- tests/components/simplisafe/conftest.py | 4 +++- tests/components/simplisafe/test_diagnostics.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py index dac89715c10..cd6e4ca52be 100644 --- a/homeassistant/components/simplisafe/diagnostics.py +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_LOCATION +from homeassistant.const import CONF_ADDRESS, CONF_CODE, CONF_LOCATION from homeassistant.core import HomeAssistant from . import SimpliSafe @@ -23,6 +23,7 @@ CONF_WIFI_SSID = "wifi_ssid" TO_REDACT = { CONF_ADDRESS, + CONF_CODE, CONF_CREDIT_CARD, CONF_EXPIRES, CONF_LOCATION, diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index 56967ac24c5..82bd04a7349 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -43,7 +43,9 @@ def api_fixture(api_auth_state, data_subscription, system_v3, websocket): @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config, unique_id): """Define a config entry.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry( + domain=DOMAIN, unique_id=unique_id, data=config, options={CONF_CODE: "1234"} + ) entry.add_to_hass(hass) return entry diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index 13d5c778e89..446d9d5e9e3 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -8,7 +8,9 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { - "options": {}, + "options": { + "code": REDACTED, + }, }, "subscription_data": { "system_123": { From 755abbe2d0bf7e757eeae8c770b85afddcf83f7c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 13 Jul 2022 22:05:43 +0200 Subject: [PATCH 417/820] Make sure device tuple is a list on save (#75103) --- homeassistant/components/rfxtrx/config_flow.py | 2 +- tests/components/rfxtrx/test_config_flow.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 61d01b8d533..508ca9a7037 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -199,7 +199,7 @@ class OptionsFlow(config_entries.OptionsFlow): if not errors: devices = {} device = { - CONF_DEVICE_ID: device_id, + CONF_DEVICE_ID: list(device_id), } devices[self._selected_device_event_code] = device diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 555d780fe92..7679dbbf37e 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -867,6 +867,9 @@ async def test_options_configure_rfy_cover_device(hass): entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] == "EU" ) + assert isinstance( + entry.data["devices"]["0C1a0000010203010000000000"]["device_id"], list + ) device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) @@ -904,6 +907,9 @@ async def test_options_configure_rfy_cover_device(hass): entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] == "EU" ) + assert isinstance( + entry.data["devices"]["0C1a0000010203010000000000"]["device_id"], list + ) def test_get_serial_by_id_no_dir(): From d1ffc7e9e312552800b2adcfffcccbda11b4d2d1 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 14 Jul 2022 06:06:32 +1000 Subject: [PATCH 418/820] Fix Powerview top shade open position (#75110) --- .../hunterdouglas_powerview/cover.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 3f7d04f5b87..7c4c90a2132 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -455,14 +455,6 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): super().__init__(coordinator, device_info, room_name, shade, name) self._attr_unique_id = f"{self._shade.id}_top" self._attr_name = f"{self._shade_name} Top" - # these shades share a class in parent API - # override open position for top shade - self._shade.open_position = { - ATTR_POSITION1: MIN_POSITION, - ATTR_POSITION2: MAX_POSITION, - ATTR_POSKIND1: POS_KIND_PRIMARY, - ATTR_POSKIND2: POS_KIND_SECONDARY, - } @property def should_poll(self) -> bool: @@ -485,6 +477,21 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): # these need to be inverted to report state correctly in HA return hd_position_to_hass(self.positions.secondary, MAX_POSITION) + @property + def open_position(self) -> PowerviewShadeMove: + """Return the open position and required additional positions.""" + # these shades share a class in parent API + # override open position for top shade + return PowerviewShadeMove( + { + ATTR_POSITION1: MIN_POSITION, + ATTR_POSITION2: MAX_POSITION, + ATTR_POSKIND1: POS_KIND_PRIMARY, + ATTR_POSKIND2: POS_KIND_SECONDARY, + }, + {}, + ) + @callback def _clamp_cover_limit(self, target_hass_position: int) -> int: """Dont allow a cover to go into an impossbile position.""" From 1768315c500681f874f7d95c64cb041802769925 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Jul 2022 14:12:53 -0700 Subject: [PATCH 419/820] Block bad pubnub version (#75138) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c92bf170f45..72d88ab7cf5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -114,3 +114,7 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 + +# Breaks asyncio +# https://github.com/pubnub/python/issues/130 +pubnub!=6.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 10951bcc7e8..7bdecd9e0c2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -132,6 +132,10 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 + +# Breaks asyncio +# https://github.com/pubnub/python/issues/130 +pubnub!=6.4.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 3166d4895ffcbc3c26703dc65d84feec6e83f96c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 13 Jul 2022 17:13:09 -0400 Subject: [PATCH 420/820] Bump ZHA dependencies (#75133) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c97cca8deb3..ce1da9071b4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.0", + "bellows==0.31.1", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.77", @@ -12,7 +12,7 @@ "zigpy==0.47.2", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", - "zigpy-znp==0.8.0" + "zigpy-znp==0.8.1" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 5dd7d0f871f..380a8d24f21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -393,7 +393,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.0 +bellows==0.31.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 @@ -2519,7 +2519,7 @@ zigpy-xbee==0.15.0 zigpy-zigate==0.9.0 # homeassistant.components.zha -zigpy-znp==0.8.0 +zigpy-znp==0.8.1 # homeassistant.components.zha zigpy==0.47.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3bb3335510..5469672ee54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -311,7 +311,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.0 +bellows==0.31.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 @@ -1683,7 +1683,7 @@ zigpy-xbee==0.15.0 zigpy-zigate==0.9.0 # homeassistant.components.zha -zigpy-znp==0.8.0 +zigpy-znp==0.8.1 # homeassistant.components.zha zigpy==0.47.2 From 1d2e64e3dc724184d56916143c5b2d453021fc22 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 14 Jul 2022 00:27:41 +0000 Subject: [PATCH 421/820] [ci skip] Translation update --- .../airvisual/translations/sensor.pt.json | 7 +++++ .../components/anthemav/translations/ru.json | 19 ++++++++++++ .../aussie_broadband/translations/pt.json | 5 ++++ .../binary_sensor/translations/pt.json | 3 +- .../components/bosch_shc/translations/pt.json | 3 +- .../components/brunt/translations/pt.json | 1 + .../demo/translations/select.pt.json | 7 +++++ .../derivative/translations/sv.json | 29 +++++++++++++++++-- .../components/dlna_dmr/translations/pt.json | 3 ++ .../components/dlna_dms/translations/pt.json | 1 + .../components/dsmr/translations/pt.json | 3 +- .../components/econet/translations/pt.json | 3 +- .../components/efergy/translations/pt.json | 14 +++++++-- .../components/emonitor/translations/pt.json | 4 +++ .../components/energy/translations/pt.json | 3 ++ .../components/fibaro/translations/pt.json | 3 ++ .../components/fritz/translations/pt.json | 3 ++ .../components/generic/translations/no.json | 3 ++ .../components/generic/translations/ru.json | 4 +++ .../components/group/translations/pt.json | 5 ++++ .../here_travel_time/translations/no.json | 5 ++++ .../here_travel_time/translations/ru.json | 7 +++++ .../homeassistant/translations/ru.json | 1 + .../components/homekit/translations/it.json | 4 +-- .../homekit_controller/translations/pt.json | 3 +- .../components/hyperion/translations/pt.json | 5 ++++ .../components/isy994/translations/pt.json | 1 + .../lg_soundbar/translations/ru.json | 18 ++++++++++++ .../components/life360/translations/ru.json | 23 +++++++++++++++ .../components/lookin/translations/pt.json | 3 ++ .../lutron_caseta/translations/pt.json | 6 ++++ .../components/nextdns/translations/no.json | 19 ++++++++++++ .../components/nextdns/translations/ru.json | 29 +++++++++++++++++++ .../components/nuki/translations/pt.json | 3 +- .../components/onewire/translations/pt.json | 3 +- .../ovo_energy/translations/pt.json | 4 ++- .../components/pvoutput/translations/pt.json | 3 +- .../pvpc_hourly_pricing/translations/pt.json | 7 +++++ .../components/qnap_qsw/translations/ru.json | 6 ++++ .../components/rhasspy/translations/ca.json | 5 ++++ .../components/rhasspy/translations/no.json | 7 +++++ .../components/rhasspy/translations/ru.json | 12 ++++++++ .../components/sensor/translations/pt.json | 2 ++ .../components/sleepiq/translations/pt.json | 5 ++++ .../components/sma/translations/pt.json | 8 +++++ .../soundtouch/translations/ru.json | 21 ++++++++++++++ .../srp_energy/translations/pt.json | 2 ++ .../surepetcare/translations/pt.json | 1 + .../components/switchbot/translations/pt.json | 9 ++++++ .../components/tod/translations/pt.json | 11 +++++++ .../tuya/translations/select.pt.json | 6 +++- .../unifiprotect/translations/pt.json | 1 + .../utility_meter/translations/pt.json | 13 +++++++++ .../components/wallbox/translations/pt.json | 7 +++++ .../components/withings/translations/ca.json | 1 + .../components/withings/translations/no.json | 3 ++ .../components/withings/translations/pt.json | 3 +- .../components/withings/translations/ru.json | 4 +++ .../components/zha/translations/pt.json | 5 ++++ .../zoneminder/translations/pt.json | 1 + .../components/zwave_js/translations/pt.json | 5 ++++ 61 files changed, 388 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/airvisual/translations/sensor.pt.json create mode 100644 homeassistant/components/anthemav/translations/ru.json create mode 100644 homeassistant/components/demo/translations/select.pt.json create mode 100644 homeassistant/components/energy/translations/pt.json create mode 100644 homeassistant/components/lg_soundbar/translations/ru.json create mode 100644 homeassistant/components/nextdns/translations/no.json create mode 100644 homeassistant/components/nextdns/translations/ru.json create mode 100644 homeassistant/components/rhasspy/translations/no.json create mode 100644 homeassistant/components/rhasspy/translations/ru.json create mode 100644 homeassistant/components/sma/translations/pt.json create mode 100644 homeassistant/components/soundtouch/translations/ru.json create mode 100644 homeassistant/components/tod/translations/pt.json create mode 100644 homeassistant/components/utility_meter/translations/pt.json diff --git a/homeassistant/components/airvisual/translations/sensor.pt.json b/homeassistant/components/airvisual/translations/sensor.pt.json new file mode 100644 index 00000000000..08a2e0c018e --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "airvisual__pollutant_level": { + "moderate": "Moderado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ru.json b/homeassistant/components/anthemav/translations/ru.json new file mode 100644 index 00000000000..0f343609e4c --- /dev/null +++ b/homeassistant/components/anthemav/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "cannot_receive_deviceinfo": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c MAC-\u0430\u0434\u0440\u0435\u0441. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/pt.json b/homeassistant/components/aussie_broadband/translations/pt.json index e831e2ce397..5a8312a5ac1 100644 --- a/homeassistant/components/aussie_broadband/translations/pt.json +++ b/homeassistant/components/aussie_broadband/translations/pt.json @@ -5,6 +5,11 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "username": "Nome de Utilizador" diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index a91c4987abb..470acfff5db 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -90,7 +90,8 @@ } }, "device_class": { - "moisture": "humidade" + "moisture": "humidade", + "problem": "problema" }, "state": { "_": { diff --git a/homeassistant/components/bosch_shc/translations/pt.json b/homeassistant/components/bosch_shc/translations/pt.json index 1286d95f2df..e229572938d 100644 --- a/homeassistant/components/bosch_shc/translations/pt.json +++ b/homeassistant/components/bosch_shc/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/brunt/translations/pt.json b/homeassistant/components/brunt/translations/pt.json index a27df267700..6f18afa4df3 100644 --- a/homeassistant/components/brunt/translations/pt.json +++ b/homeassistant/components/brunt/translations/pt.json @@ -4,6 +4,7 @@ "already_configured": "Conta j\u00e1 configurada" }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/demo/translations/select.pt.json b/homeassistant/components/demo/translations/select.pt.json new file mode 100644 index 00000000000..438f02fa47f --- /dev/null +++ b/homeassistant/components/demo/translations/select.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Velocidade da Luz" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/sv.json b/homeassistant/components/derivative/translations/sv.json index 66dcd34b1d7..ac5b766d124 100644 --- a/homeassistant/components/derivative/translations/sv.json +++ b/homeassistant/components/derivative/translations/sv.json @@ -1,12 +1,37 @@ { + "config": { + "step": { + "user": { + "data": { + "name": "Namn", + "round": "Precision", + "time_window": "Tidsf\u00f6nster", + "unit_prefix": "Metriskt prefix", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Anger antal decimaler i resultatet." + }, + "description": "Skapa en sensor som ber\u00e4knar derivatan av en sensor", + "title": "L\u00e4gg till derivatasensor" + } + } + }, "options": { "step": { "init": { "data": { "name": "Namn", - "round": "Precision" + "round": "Precision", + "time_window": "Tidsf\u00f6nster", + "unit_prefix": "Metriskt prefix", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Anger antal decimaler i resultatet." } } } - } + }, + "title": "Derivatasensor" } \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/pt.json b/homeassistant/components/dlna_dmr/translations/pt.json index f6f3f84e497..49f47abb540 100644 --- a/homeassistant/components/dlna_dmr/translations/pt.json +++ b/homeassistant/components/dlna_dmr/translations/pt.json @@ -4,6 +4,9 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "confirm": { + "description": "Autentica\u00e7\u00e3o inv\u00e1lid" + }, "manual": { "data": { "url": "" diff --git a/homeassistant/components/dlna_dms/translations/pt.json b/homeassistant/components/dlna_dms/translations/pt.json index f67e36f3487..1a5e788da4b 100644 --- a/homeassistant/components/dlna_dms/translations/pt.json +++ b/homeassistant/components/dlna_dms/translations/pt.json @@ -4,6 +4,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/dsmr/translations/pt.json b/homeassistant/components/dsmr/translations/pt.json index c9fff00bd53..311a677d6de 100644 --- a/homeassistant/components/dsmr/translations/pt.json +++ b/homeassistant/components/dsmr/translations/pt.json @@ -11,7 +11,8 @@ "setup_network": { "data": { "host": "Servidor" - } + }, + "title": "Seleccione o endere\u00e7o de liga\u00e7\u00e3o" }, "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/econet/translations/pt.json b/homeassistant/components/econet/translations/pt.json index 6c333b17fc9..25aaf514180 100644 --- a/homeassistant/components/econet/translations/pt.json +++ b/homeassistant/components/econet/translations/pt.json @@ -7,7 +7,8 @@ "step": { "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Palavra-passe" } } } diff --git a/homeassistant/components/efergy/translations/pt.json b/homeassistant/components/efergy/translations/pt.json index 8f319572c97..db89dfe29e6 100644 --- a/homeassistant/components/efergy/translations/pt.json +++ b/homeassistant/components/efergy/translations/pt.json @@ -1,10 +1,20 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave API" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/pt.json b/homeassistant/components/emonitor/translations/pt.json index 8578f969852..04374af8e82 100644 --- a/homeassistant/components/emonitor/translations/pt.json +++ b/homeassistant/components/emonitor/translations/pt.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/energy/translations/pt.json b/homeassistant/components/energy/translations/pt.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/pt.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/pt.json b/homeassistant/components/fibaro/translations/pt.json index ce8a9287272..db0e0c2a137 100644 --- a/homeassistant/components/fibaro/translations/pt.json +++ b/homeassistant/components/fibaro/translations/pt.json @@ -2,6 +2,9 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" } } } \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/pt.json b/homeassistant/components/fritz/translations/pt.json index 263485b4587..b191e607a42 100644 --- a/homeassistant/components/fritz/translations/pt.json +++ b/homeassistant/components/fritz/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index 72355d002ed..5c718dc0f47 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Et kamera med disse URL-innstillingene finnes allerede.", "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", + "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", + "relative_url": "Relative URL-adresser ikke tillatt", "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", "stream_io_error": "Inn-/utdatafeil under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", @@ -50,6 +52,7 @@ "error": { "already_exists": "Et kamera med disse URL-innstillingene finnes allerede.", "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", + "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json index f9fed6b5831..7314a172867 100644 --- a/homeassistant/components/generic/translations/ru.json +++ b/homeassistant/components/generic/translations/ru.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u041a\u0430\u043c\u0435\u0440\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c URL-\u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", "invalid_still_image": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435.", + "malformed_url": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", + "relative_url": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f.", "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", @@ -49,7 +51,9 @@ "error": { "already_exists": "\u041a\u0430\u043c\u0435\u0440\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c URL-\u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", "invalid_still_image": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435.", + "malformed_url": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", + "relative_url": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f.", "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index b05b2868744..57c8a41e630 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -14,6 +14,11 @@ "data": { "hide_members": "Esconder membros" } + }, + "switch": { + "data": { + "hide_members": "Esconder membros" + } } } }, diff --git a/homeassistant/components/here_travel_time/translations/no.json b/homeassistant/components/here_travel_time/translations/no.json index 52d4477f379..e4282933051 100644 --- a/homeassistant/components/here_travel_time/translations/no.json +++ b/homeassistant/components/here_travel_time/translations/no.json @@ -39,6 +39,11 @@ }, "title": "Velg Opprinnelse" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Bruk kartplassering" + } + }, "user": { "data": { "api_key": "API-n\u00f8kkel", diff --git a/homeassistant/components/here_travel_time/translations/ru.json b/homeassistant/components/here_travel_time/translations/ru.json index fc649d920df..a89608530d8 100644 --- a/homeassistant/components/here_travel_time/translations/ru.json +++ b/homeassistant/components/here_travel_time/translations/ru.json @@ -39,6 +39,13 @@ }, "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u0423\u043a\u0430\u0437\u0430\u0442\u044c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", + "origin_entity": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/homeassistant/translations/ru.json b/homeassistant/components/homeassistant/translations/ru.json index f8932f1ea7d..b0e27b70861 100644 --- a/homeassistant/components/homeassistant/translations/ru.json +++ b/homeassistant/components/homeassistant/translations/ru.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", + "config_dir": "\u041a\u0430\u0442\u0430\u043b\u043e\u0433 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", "dev": "\u0421\u0440\u0435\u0434\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 4086db071c2..c46acd8965d 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -44,14 +44,14 @@ "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domini}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", "title": "Seleziona le entit\u00e0 da escludere" }, "include": { "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domini}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", "title": "Seleziona le entit\u00e0 da includere" }, "init": { diff --git a/homeassistant/components/homekit_controller/translations/pt.json b/homeassistant/components/homekit_controller/translations/pt.json index c4ab5c1e636..febadec444c 100644 --- a/homeassistant/components/homekit_controller/translations/pt.json +++ b/homeassistant/components/homekit_controller/translations/pt.json @@ -48,7 +48,8 @@ "button6": "Bot\u00e3o 6", "button7": "Bot\u00e3o 7", "button8": "Bot\u00e3o 8", - "button9": "Bot\u00e3o 9" + "button9": "Bot\u00e3o 9", + "doorbell": "Campainha" }, "trigger_type": { "single_press": "\"{subtype}\" pressionado" diff --git a/homeassistant/components/hyperion/translations/pt.json b/homeassistant/components/hyperion/translations/pt.json index ac9710c6b9b..0a402a87037 100644 --- a/homeassistant/components/hyperion/translations/pt.json +++ b/homeassistant/components/hyperion/translations/pt.json @@ -10,6 +10,11 @@ "invalid_access_token": "Token de acesso inv\u00e1lido" }, "step": { + "auth": { + "data": { + "create_token": "Criar novo token automaticamente" + } + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json index 9f8734b5a6d..aa4c5f614e6 100644 --- a/homeassistant/components/isy994/translations/pt.json +++ b/homeassistant/components/isy994/translations/pt.json @@ -8,6 +8,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, + "flow_title": "{name} ({host})", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/lg_soundbar/translations/ru.json b/homeassistant/components/lg_soundbar/translations/ru.json new file mode 100644 index 00000000000..f7961eb2e7e --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index 4b3fbceb5d6..1f1f92977ed 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", @@ -23,5 +32,19 @@ "title": "Life360" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u043a\u0430\u043a \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", + "driving_speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u0432\u043e\u0436\u0434\u0435\u043d\u0438\u044f", + "limit_gps_acc": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0442\u044c \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c GPS", + "max_gps_accuracy": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c GPS (\u0432 \u043c\u0435\u0442\u0440\u0430\u0445)", + "set_drive_speed": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u043e\u0440\u043e\u0433 \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/pt.json b/homeassistant/components/lookin/translations/pt.json index 28ad8b6c8f3..03fd6fa7632 100644 --- a/homeassistant/components/lookin/translations/pt.json +++ b/homeassistant/components/lookin/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/lutron_caseta/translations/pt.json b/homeassistant/components/lutron_caseta/translations/pt.json index f4fdf676dbd..4ae57417d6f 100644 --- a/homeassistant/components/lutron_caseta/translations/pt.json +++ b/homeassistant/components/lutron_caseta/translations/pt.json @@ -14,5 +14,11 @@ } } } + }, + "device_automation": { + "trigger_subtype": { + "on": "Ligado", + "raise_3": "Aumentar 3" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/no.json b/homeassistant/components/nextdns/translations/no.json new file mode 100644 index 00000000000..fb4d6616587 --- /dev/null +++ b/homeassistant/components/nextdns/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "unknown": "Totalt uventet feil" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ru.json b/homeassistant/components/nextdns/translations/ru.json new file mode 100644 index 00000000000..952058cf5f6 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e\u0442 \u043f\u0440\u043e\u0444\u0438\u043b\u044c NextDNS \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "profiles": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" + } + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/pt.json b/homeassistant/components/nuki/translations/pt.json index ce7cbc3f548..f681da4210f 100644 --- a/homeassistant/components/nuki/translations/pt.json +++ b/homeassistant/components/nuki/translations/pt.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/onewire/translations/pt.json b/homeassistant/components/onewire/translations/pt.json index 91786f4b324..fa5aa3de317 100644 --- a/homeassistant/components/onewire/translations/pt.json +++ b/homeassistant/components/onewire/translations/pt.json @@ -9,7 +9,8 @@ "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/ovo_energy/translations/pt.json b/homeassistant/components/ovo_energy/translations/pt.json index 7015a44b5f9..15241a5fb65 100644 --- a/homeassistant/components/ovo_energy/translations/pt.json +++ b/homeassistant/components/ovo_energy/translations/pt.json @@ -5,11 +5,13 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, + "flow_title": "{username}", "step": { "reauth": { "data": { "password": "Palavra-passe" - } + }, + "title": "Reautentica\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/pvoutput/translations/pt.json b/homeassistant/components/pvoutput/translations/pt.json index 3b5850222d9..98fe3611480 100644 --- a/homeassistant/components/pvoutput/translations/pt.json +++ b/homeassistant/components/pvoutput/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json index d252c078a2c..79ce20e5033 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pt.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "power": "Pot\u00eancia contratada (kW)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/ru.json b/homeassistant/components/qnap_qsw/translations/ru.json index ae3869552d0..b45585ca3a4 100644 --- a/homeassistant/components/qnap_qsw/translations/ru.json +++ b/homeassistant/components/qnap_qsw/translations/ru.json @@ -9,6 +9,12 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { + "discovered_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/rhasspy/translations/ca.json b/homeassistant/components/rhasspy/translations/ca.json index cc92e3ec9f1..b016b21a2e2 100644 --- a/homeassistant/components/rhasspy/translations/ca.json +++ b/homeassistant/components/rhasspy/translations/ca.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols activar el suport de Rhasspy?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/no.json b/homeassistant/components/rhasspy/translations/no.json new file mode 100644 index 00000000000..4dc3393a982 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/no.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Kun \u00e9n konfigurert instans i gangen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/ru.json b/homeassistant/components/rhasspy/translations/ru.json new file mode 100644 index 00000000000..ce319f9537e --- /dev/null +++ b/homeassistant/components/rhasspy/translations/ru.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/pt.json b/homeassistant/components/sensor/translations/pt.json index eaef8ef5755..0e162b9c7ca 100644 --- a/homeassistant/components/sensor/translations/pt.json +++ b/homeassistant/components/sensor/translations/pt.json @@ -2,6 +2,7 @@ "device_automation": { "condition_type": { "is_battery_level": "N\u00edvel de bateria atual de {entity_name}", + "is_energy": "Energia atual de {entity_name}", "is_humidity": "humidade {entity_name}", "is_illuminance": "Luminancia atual de {entity_name}", "is_power": "Pot\u00eancia atual de {entity_name}", @@ -12,6 +13,7 @@ }, "trigger_type": { "battery_level": "n\u00edvel da bateria {entity_name}", + "energy": "Mudan\u00e7as de energia de {entity_name}", "humidity": "humidade {entity_name}", "illuminance": "ilumin\u00e2ncia {entity_name}", "power": "pot\u00eancia {entity_name}", diff --git a/homeassistant/components/sleepiq/translations/pt.json b/homeassistant/components/sleepiq/translations/pt.json index 5602f898fc3..cf42abdb666 100644 --- a/homeassistant/components/sleepiq/translations/pt.json +++ b/homeassistant/components/sleepiq/translations/pt.json @@ -9,6 +9,11 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/sma/translations/pt.json b/homeassistant/components/sma/translations/pt.json new file mode 100644 index 00000000000..a37e6656da7 --- /dev/null +++ b/homeassistant/components/sma/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ru.json b/homeassistant/components/soundtouch/translations/ru.json new file mode 100644 index 00000000000..d987f817a85 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 Home Assistant \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e SoundTouch \u0441 \u0438\u043c\u0435\u043d\u0435\u043c `{name}`.", + "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/pt.json b/homeassistant/components/srp_energy/translations/pt.json index 3e10b977773..15c3b188dec 100644 --- a/homeassistant/components/srp_energy/translations/pt.json +++ b/homeassistant/components/srp_energy/translations/pt.json @@ -5,12 +5,14 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_account": "O ID da conta deve ser um n\u00famero de 9 d\u00edgitos", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { + "id": "ID da conta", "password": "Palavra-passe", "username": "Nome de Utilizador" } diff --git a/homeassistant/components/surepetcare/translations/pt.json b/homeassistant/components/surepetcare/translations/pt.json index 0e89c76d047..565b9f6c0e8 100644 --- a/homeassistant/components/surepetcare/translations/pt.json +++ b/homeassistant/components/surepetcare/translations/pt.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/switchbot/translations/pt.json b/homeassistant/components/switchbot/translations/pt.json index b8a454fbaba..e2dc9fee9b4 100644 --- a/homeassistant/components/switchbot/translations/pt.json +++ b/homeassistant/components/switchbot/translations/pt.json @@ -7,5 +7,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "update_time": "Tempo entre actualiza\u00e7\u00f5es (segundos)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tod/translations/pt.json b/homeassistant/components/tod/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/tod/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.pt.json b/homeassistant/components/tuya/translations/select.pt.json index fac54570cd6..3a66717f219 100644 --- a/homeassistant/components/tuya/translations/select.pt.json +++ b/homeassistant/components/tuya/translations/select.pt.json @@ -3,6 +3,9 @@ "tuya__basic_nightvision": { "1": "Desligado" }, + "tuya__countdown": { + "cancel": "Cancelar" + }, "tuya__light_mode": { "none": "Desligado" }, @@ -16,7 +19,8 @@ }, "tuya__vacuum_mode": { "pick_zone": "Escolha a Zona", - "right_spiral": "Espiral Direita" + "right_spiral": "Espiral Direita", + "spiral": "Espiral" } } } \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/pt.json b/homeassistant/components/unifiprotect/translations/pt.json index 253a0f414df..9f781c2de55 100644 --- a/homeassistant/components/unifiprotect/translations/pt.json +++ b/homeassistant/components/unifiprotect/translations/pt.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { diff --git a/homeassistant/components/utility_meter/translations/pt.json b/homeassistant/components/utility_meter/translations/pt.json new file mode 100644 index 00000000000..1ad401c42e6 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "tariffs": "Tarifas suportadas" + }, + "description": "Criar um sensor que monitorize o consumo de v\u00e1rias utilidades (por exemplo, energia, g\u00e1s, \u00e1gua, aquecimento) durante um per\u00edodo configurado, tipicamente mensal. O sensor de contador de utilidades (utility_meter) suporta opcionalmente a divis\u00e3o do consumo por tarifas; nesse caso \u00e9 criado um sensor para cada tarifa, bem como uma entidade selecionada para escolher a tarifa atual." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/pt.json b/homeassistant/components/wallbox/translations/pt.json index ce8a9287272..ece60e5e010 100644 --- a/homeassistant/components/wallbox/translations/pt.json +++ b/homeassistant/components/wallbox/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index c7ec9fcd8ca..91be6ffabbe 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -29,6 +29,7 @@ "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, "reauth_confirm": { + "description": "El perfil \"{profile}\" s'ha de tornar a autenticar per poder continuar rebent dades de Withings.", "title": "Reautenticar la integraci\u00f3" } } diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index ff8d18eec64..40ba24111bf 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -27,6 +27,9 @@ "reauth": { "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", "title": "Godkjenne integrering p\u00e5 nytt" + }, + "reauth_confirm": { + "title": "Re-autentiser integrasjon" } } } diff --git a/homeassistant/components/withings/translations/pt.json b/homeassistant/components/withings/translations/pt.json index 673627c8036..fa97013c5c0 100644 --- a/homeassistant/components/withings/translations/pt.json +++ b/homeassistant/components/withings/translations/pt.json @@ -15,7 +15,8 @@ "profile": { "data": { "profile": "Perfil" - } + }, + "description": "Fornecer um nome de perfil \u00fanico para estes dados. Normalmente, este \u00e9 o nome do perfil que seleccionou na etapa anterior." }, "reauth": { "title": "Re-autenticar Perfil" diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index 7127f9545fa..78f8f3ec5e4 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -27,6 +27,10 @@ "reauth": { "description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "reauth_confirm": { + "description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/zha/translations/pt.json b/homeassistant/components/zha/translations/pt.json index 42a5c292ecb..435e80b8b76 100644 --- a/homeassistant/components/zha/translations/pt.json +++ b/homeassistant/components/zha/translations/pt.json @@ -12,6 +12,11 @@ } } }, + "config_panel": { + "zha_options": { + "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz predefinido (segundos)" + } + }, "device_automation": { "action_type": { "warn": "Avisar" diff --git a/homeassistant/components/zoneminder/translations/pt.json b/homeassistant/components/zoneminder/translations/pt.json index f8fa0efe967..b85c7c2e7a7 100644 --- a/homeassistant/components/zoneminder/translations/pt.json +++ b/homeassistant/components/zoneminder/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "auth_fail": "O nome de utilizador ou palavra-passe est\u00e1 incorrecto.", "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, diff --git a/homeassistant/components/zwave_js/translations/pt.json b/homeassistant/components/zwave_js/translations/pt.json index 086d4c19ebb..dc59810f31e 100644 --- a/homeassistant/components/zwave_js/translations/pt.json +++ b/homeassistant/components/zwave_js/translations/pt.json @@ -12,6 +12,11 @@ "unknown": "Erro inesperado" }, "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emular Hardware" + } + }, "manual": { "data": { "url": "" From 2aa98da6246769571af0e6cd8b2fdd2bc8ab04a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 10:56:01 +0200 Subject: [PATCH 422/820] Migrate Whois to new entity naming style (#75019) --- homeassistant/components/whois/sensor.py | 7 ++++--- tests/components/whois/test_sensor.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 48efaf7630d..68e77162a4f 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -79,7 +79,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), WhoisSensorEntityDescription( key="days_until_expiration", - name="Days Until Expiration", + name="Days until expiration", icon="mdi:calendar-clock", native_unit_of_measurement=TIME_DAYS, value_fn=_days_until_expiration, @@ -93,7 +93,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), WhoisSensorEntityDescription( key="last_updated", - name="Last Updated", + name="Last updated", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda domain: _ensure_timezone(domain.last_updated), @@ -156,6 +156,7 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity): """Implementation of a WHOIS sensor.""" entity_description: WhoisSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -166,10 +167,10 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity): """Initialize the sensor.""" super().__init__(coordinator=coordinator) self.entity_description = description - self._attr_name = f"{domain} {description.name}" self._attr_unique_id = f"{domain}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, domain)}, + name=domain, entry_type=DeviceEntryType.SERVICE, ) self._domain = domain diff --git a/tests/components/whois/test_sensor.py b/tests/components/whois/test_sensor.py index 89b84dc5849..828a9242621 100644 --- a/tests/components/whois/test_sensor.py +++ b/tests/components/whois/test_sensor.py @@ -60,7 +60,7 @@ async def test_whois_sensors( assert state.state == "364" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "home-assistant.io Days Until Expiration" + == "home-assistant.io Days until expiration" ) assert state.attributes.get(ATTR_ICON) == "mdi:calendar-clock" assert ATTR_DEVICE_CLASS not in state.attributes @@ -83,7 +83,7 @@ async def test_whois_sensors( assert entry.unique_id == "home-assistant.io_last_updated" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "2021-12-31T23:00:00+00:00" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last Updated" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last updated" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes @@ -137,9 +137,9 @@ async def test_whois_sensors( assert device_entry.configuration_url is None assert device_entry.entry_type == dr.DeviceEntryType.SERVICE assert device_entry.identifiers == {(DOMAIN, "home-assistant.io")} + assert device_entry.name == "home-assistant.io" assert device_entry.manufacturer is None assert device_entry.model is None - assert device_entry.name is None assert device_entry.sw_version is None @@ -159,7 +159,7 @@ async def test_whois_sensors_missing_some_attrs( assert entry.unique_id == "home-assistant.io_last_updated" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "2021-12-31T23:00:00+00:00" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last Updated" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last updated" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes From a720b2989aab7886af64fc2be8083857a544614e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 10:56:14 +0200 Subject: [PATCH 423/820] Migrate RDW to new entity naming style (#75017) --- homeassistant/components/rdw/binary_sensor.py | 7 ++++--- homeassistant/components/rdw/sensor.py | 7 ++++--- tests/components/rdw/test_binary_sensor.py | 14 +++++++------- tests/components/rdw/test_sensor.py | 14 +++++++------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py index 81d3e448b78..25ef6b27f1d 100644 --- a/homeassistant/components/rdw/binary_sensor.py +++ b/homeassistant/components/rdw/binary_sensor.py @@ -41,13 +41,13 @@ class RDWBinarySensorEntityDescription( BINARY_SENSORS: tuple[RDWBinarySensorEntityDescription, ...] = ( RDWBinarySensorEntityDescription( key="liability_insured", - name="Liability Insured", + name="Liability insured", icon="mdi:shield-car", is_on_fn=lambda vehicle: vehicle.liability_insured, ), RDWBinarySensorEntityDescription( key="pending_recall", - name="Pending Recall", + name="Pending recall", device_class=BinarySensorDeviceClass.PROBLEM, is_on_fn=lambda vehicle: vehicle.pending_recall, ), @@ -75,6 +75,7 @@ class RDWBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): """Defines an RDW binary sensor.""" entity_description: RDWBinarySensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -91,7 +92,7 @@ class RDWBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.data.license_plate)}, manufacturer=coordinator.data.brand, - name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", + name=f"{coordinator.data.brand} {coordinator.data.license_plate}", model=coordinator.data.model, configuration_url=f"https://ovi.rdw.nl/default.aspx?kenteken={coordinator.data.license_plate}", ) diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index 04f525c61b8..d4cb97005a8 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -42,13 +42,13 @@ class RDWSensorEntityDescription( SENSORS: tuple[RDWSensorEntityDescription, ...] = ( RDWSensorEntityDescription( key="apk_expiration", - name="APK Expiration", + name="APK expiration", device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.apk_expiration, ), RDWSensorEntityDescription( key="ascription_date", - name="Ascription Date", + name="Ascription date", device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.ascription_date, ), @@ -76,6 +76,7 @@ class RDWSensorEntity(CoordinatorEntity, SensorEntity): """Defines an RDW sensor.""" entity_description: RDWSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -93,7 +94,7 @@ class RDWSensorEntity(CoordinatorEntity, SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{license_plate}")}, manufacturer=coordinator.data.brand, - name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", + name=f"{coordinator.data.brand} {coordinator.data.license_plate}", model=coordinator.data.model, configuration_url=f"https://ovi.rdw.nl/default.aspx?kenteken={coordinator.data.license_plate}", ) diff --git a/tests/components/rdw/test_binary_sensor.py b/tests/components/rdw/test_binary_sensor.py index abf15d869ce..aea188db773 100644 --- a/tests/components/rdw/test_binary_sensor.py +++ b/tests/components/rdw/test_binary_sensor.py @@ -16,23 +16,23 @@ async def test_vehicle_binary_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("binary_sensor.liability_insured") - entry = entity_registry.async_get("binary_sensor.liability_insured") + state = hass.states.get("binary_sensor.skoda_11zkz3_liability_insured") + entry = entity_registry.async_get("binary_sensor.skoda_11zkz3_liability_insured") assert entry assert state assert entry.unique_id == "11ZKZ3_liability_insured" assert state.state == "off" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Liability Insured" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 Liability insured" assert state.attributes.get(ATTR_ICON) == "mdi:shield-car" assert ATTR_DEVICE_CLASS not in state.attributes - state = hass.states.get("binary_sensor.pending_recall") - entry = entity_registry.async_get("binary_sensor.pending_recall") + state = hass.states.get("binary_sensor.skoda_11zkz3_pending_recall") + entry = entity_registry.async_get("binary_sensor.skoda_11zkz3_pending_recall") assert entry assert state assert entry.unique_id == "11ZKZ3_pending_recall" assert state.state == "off" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending Recall" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 Pending recall" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PROBLEM assert ATTR_ICON not in state.attributes @@ -41,7 +41,7 @@ async def test_vehicle_binary_sensors( assert device_entry assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} assert device_entry.manufacturer == "Skoda" - assert device_entry.name == "Skoda: 11ZKZ3" + assert device_entry.name == "Skoda 11ZKZ3" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "Citigo" assert ( diff --git a/tests/components/rdw/test_sensor.py b/tests/components/rdw/test_sensor.py index 32d4c368d73..3e7ad7ab89e 100644 --- a/tests/components/rdw/test_sensor.py +++ b/tests/components/rdw/test_sensor.py @@ -21,25 +21,25 @@ async def test_vehicle_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.apk_expiration") - entry = entity_registry.async_get("sensor.apk_expiration") + state = hass.states.get("sensor.skoda_11zkz3_apk_expiration") + entry = entity_registry.async_get("sensor.skoda_11zkz3_apk_expiration") assert entry assert state assert entry.unique_id == "11ZKZ3_apk_expiration" assert state.state == "2022-01-04" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "APK Expiration" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 APK expiration" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert ATTR_ICON not in state.attributes assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.ascription_date") - entry = entity_registry.async_get("sensor.ascription_date") + state = hass.states.get("sensor.skoda_11zkz3_ascription_date") + entry = entity_registry.async_get("sensor.skoda_11zkz3_ascription_date") assert entry assert state assert entry.unique_id == "11ZKZ3_ascription_date" assert state.state == "2021-11-04" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ascription Date" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 Ascription date" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert ATTR_ICON not in state.attributes assert ATTR_STATE_CLASS not in state.attributes @@ -50,7 +50,7 @@ async def test_vehicle_sensors( assert device_entry assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} assert device_entry.manufacturer == "Skoda" - assert device_entry.name == "Skoda: 11ZKZ3" + assert device_entry.name == "Skoda 11ZKZ3" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "Citigo" assert ( From 08ff1b89862594077c68e16e3010b908c7880ad7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 11:05:52 +0200 Subject: [PATCH 424/820] Fix flapping system log test (#75111) --- .../components/system_log/__init__.py | 65 +++----- homeassistant/components/zha/core/gateway.py | 12 +- tests/components/network/conftest.py | 6 +- tests/components/system_log/test_init.py | 153 +++++++++--------- tests/conftest.py | 20 +++ 5 files changed, 135 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 343830fe690..b0d538a4ff8 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -1,7 +1,6 @@ """Support for system log.""" from collections import OrderedDict, deque import logging -import queue import re import traceback @@ -9,7 +8,7 @@ import voluptuous as vol from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components import websocket_api -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -56,8 +55,7 @@ SERVICE_WRITE_SCHEMA = vol.Schema( ) -def _figure_out_source(record, call_stack, hass): - paths = [HOMEASSISTANT_PATH[0], hass.config.config_dir] +def _figure_out_source(record, call_stack, paths_re): # If a stack trace exists, extract file names from the entire call stack. # The other case is when a regular "log" is made (without an attached @@ -78,11 +76,10 @@ def _figure_out_source(record, call_stack, hass): # Iterate through the stack call (in reverse) and find the last call from # a file in Home Assistant. Try to figure out where error happened. - paths_re = r"(?:{})/(.*)".format("|".join([re.escape(x) for x in paths])) for pathname in reversed(stack): # Try to match with a file within Home Assistant - if match := re.match(paths_re, pathname[0]): + if match := paths_re.match(pathname[0]): return [match.group(1), pathname[1]] # Ok, we don't know what this is return (record.pathname, record.lineno) @@ -157,26 +154,16 @@ class DedupStore(OrderedDict): return [value.to_dict() for value in reversed(self.values())] -class LogErrorQueueHandler(logging.handlers.QueueHandler): - """Process the log in another thread.""" - - def emit(self, record): - """Emit a log record.""" - try: - self.enqueue(record) - except Exception: # pylint: disable=broad-except - self.handleError(record) - - class LogErrorHandler(logging.Handler): """Log handler for error messages.""" - def __init__(self, hass, maxlen, fire_event): + def __init__(self, hass, maxlen, fire_event, paths_re): """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass self.records = DedupStore(maxlen=maxlen) self.fire_event = fire_event + self.paths_re = paths_re def emit(self, record): """Save error and warning logs. @@ -189,7 +176,9 @@ class LogErrorHandler(logging.Handler): if not record.exc_info: stack = [(f[0], f[1]) for f in traceback.extract_stack()] - entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass)) + entry = LogEntry( + record, stack, _figure_out_source(record, stack, self.paths_re) + ) self.records.add_entry(entry) if self.fire_event: self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict()) @@ -200,29 +189,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if (conf := config.get(DOMAIN)) is None: conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - simple_queue: queue.SimpleQueue = queue.SimpleQueue() - queue_handler = LogErrorQueueHandler(simple_queue) - queue_handler.setLevel(logging.WARN) - logging.root.addHandler(queue_handler) - - handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT]) + hass_path: str = HOMEASSISTANT_PATH[0] + config_dir = hass.config.config_dir + assert config_dir is not None + paths_re = re.compile( + r"(?:{})/(.*)".format("|".join([re.escape(x) for x in (hass_path, config_dir)])) + ) + handler = LogErrorHandler( + hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT], paths_re + ) + handler.setLevel(logging.WARN) hass.data[DOMAIN] = handler - listener = logging.handlers.QueueListener( - simple_queue, handler, respect_handler_level=True - ) - - listener.start() - @callback - def _async_stop_queue_handler(_) -> None: + def _async_stop_handler(_) -> None: """Cleanup handler.""" - logging.root.removeHandler(queue_handler) - listener.stop() + logging.root.removeHandler(handler) del hass.data[DOMAIN] - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_queue_handler) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_handler) + + logging.root.addHandler(handler) websocket_api.async_register_command(hass, list_errors) @@ -238,13 +226,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: level = service.data[CONF_LEVEL] getattr(logger, level)(service.data[CONF_MESSAGE]) - async def async_shutdown_handler(event): - """Remove logging handler when Home Assistant is shutdown.""" - # This is needed as older logger instances will remain - logging.getLogger().removeHandler(handler) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown_handler) - hass.services.async_register( DOMAIN, SERVICE_CLEAR, async_service_handler, schema=SERVICE_CLEAR_SCHEMA ) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 9efb6e99550..f2fd226249b 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -8,6 +8,7 @@ from datetime import timedelta from enum import Enum import itertools import logging +import re import time import traceback from typing import TYPE_CHECKING, Any, NamedTuple, Union @@ -20,6 +21,7 @@ import zigpy.endpoint import zigpy.group from zigpy.types.named import EUI64 +from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -732,7 +734,15 @@ class LogRelayHandler(logging.Handler): if record.levelno >= logging.WARN and not record.exc_info: stack = [f for f, _, _, _ in traceback.extract_stack()] - entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass)) + hass_path: str = HOMEASSISTANT_PATH[0] + config_dir = self.hass.config.config_dir + assert config_dir is not None + paths_re = re.compile( + r"(?:{})/(.*)".format( + "|".join([re.escape(x) for x in (hass_path, config_dir)]) + ) + ) + entry = LogEntry(record, stack, _figure_out_source(record, stack, paths_re)) async_dispatcher_send( self.hass, ZHA_GW_MSG, diff --git a/tests/components/network/conftest.py b/tests/components/network/conftest.py index 9c1bf232d7b..8b1b383ae42 100644 --- a/tests/components/network/conftest.py +++ b/tests/components/network/conftest.py @@ -6,4 +6,8 @@ import pytest @pytest.fixture(autouse=True) def mock_get_source_ip(): """Override mock of network util's async_get_source_ip.""" - return + + +@pytest.fixture(autouse=True) +def mock_network(): + """Override mock of network util's async_get_adapters.""" diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index d558d690536..6304e0ea7cf 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -1,10 +1,11 @@ """Test system log component.""" -import asyncio -import logging -import queue -from unittest.mock import MagicMock, patch +from __future__ import annotations -import pytest +import asyncio +from collections.abc import Awaitable +import logging +from typing import Any +from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log @@ -16,28 +17,6 @@ _LOGGER = logging.getLogger("test_logger") BASIC_CONFIG = {"system_log": {"max_entries": 2}} -@pytest.fixture -def simple_queue(): - """Fixture that get the queue.""" - simple_queue_fixed = queue.SimpleQueue() - with patch( - "homeassistant.components.system_log.queue.SimpleQueue", - return_value=simple_queue_fixed, - ): - yield simple_queue_fixed - - -async def _async_block_until_queue_empty(hass, sq): - # Unfortunately we are stuck with polling - await hass.async_block_till_done() - while not sq.empty(): - await asyncio.sleep(0.01) - hass.data[system_log.DOMAIN].acquire() - hass.data[system_log.DOMAIN].release() - await hass.async_block_till_done() - await hass.async_block_till_done() - - async def get_error_log(hass_ws_client): """Fetch all entries from system_log via the API.""" client = await hass_ws_client() @@ -81,66 +60,97 @@ def assert_log(log, exception, message, level): assert "timestamp" in log +class WatchLogErrorHandler(system_log.LogErrorHandler): + """WatchLogErrorHandler that watches for a message.""" + + instances: list[WatchLogErrorHandler] = [] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize HASSQueueListener.""" + super().__init__(*args, **kwargs) + self.watch_message: str | None = None + self.watch_event: asyncio.Event | None = asyncio.Event() + WatchLogErrorHandler.instances.append(self) + + def add_watcher(self, match: str) -> Awaitable: + """Add a watcher.""" + self.watch_event = asyncio.Event() + self.watch_message = match + return self.watch_event.wait() + + def handle(self, record: logging.LogRecord) -> None: + """Handle a logging record.""" + super().handle(record) + if record.message in self.watch_message: + self.watch_event.set() + + def get_frame(name): """Get log stack frame.""" return (name, 5, None, None) -async def test_normal_logs(hass, simple_queue, hass_ws_client): +async def async_setup_system_log(hass, config) -> WatchLogErrorHandler: + """Set up the system_log component.""" + WatchLogErrorHandler.instances = [] + with patch( + "homeassistant.components.system_log.LogErrorHandler", WatchLogErrorHandler + ): + await async_setup_component(hass, system_log.DOMAIN, config) + await hass.async_block_till_done() + + assert len(WatchLogErrorHandler.instances) == 1 + return WatchLogErrorHandler.instances.pop() + + +async def test_normal_logs(hass, hass_ws_client): """Test that debug and info are not logged.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.debug("debug") _LOGGER.info("info") - await _async_block_until_queue_empty(hass, simple_queue) # Assert done by get_error_log logs = await get_error_log(hass_ws_client) assert len([msg for msg in logs if msg["level"] in ("DEBUG", "INFO")]) == 0 -async def test_exception(hass, simple_queue, hass_ws_client): +async def test_exception(hass, hass_ws_client): """Test that exceptions are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() _generate_and_log_exception("exception message", "log message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "ERROR") assert log is not None assert_log(log, "exception message", "log message", "ERROR") -async def test_warning(hass, simple_queue, hass_ws_client): +async def test_warning(hass, hass_ws_client): """Test that warning are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.warning("warning message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "WARNING") assert_log(log, "", "warning message", "WARNING") -async def test_error(hass, simple_queue, hass_ws_client): +async def test_error(hass, hass_ws_client): """Test that errors are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "ERROR") assert_log(log, "", "error message", "ERROR") -async def test_config_not_fire_event(hass, simple_queue): +async def test_config_not_fire_event(hass): """Test that errors are not posted as events with default config.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - events = [] @callback @@ -150,50 +160,49 @@ async def test_config_not_fire_event(hass, simple_queue): hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) - _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) + await hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 0 -async def test_error_posted_as_event(hass, simple_queue): +async def test_error_posted_as_event(hass): """Test that error are posted as events.""" - await async_setup_component( - hass, system_log.DOMAIN, {"system_log": {"max_entries": 2, "fire_event": True}} + watcher = await async_setup_system_log( + hass, {"system_log": {"max_entries": 2, "fire_event": True}} ) - await hass.async_block_till_done() + wait_empty = watcher.add_watcher("error message") events = async_capture_events(hass, system_log.EVENT_SYSTEM_LOG) _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) + await wait_empty + await hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 1 assert_log(events[0].data, "", "error message", "ERROR") -async def test_critical(hass, simple_queue, hass_ws_client): +async def test_critical(hass, hass_ws_client): """Test that critical are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() _LOGGER.critical("critical message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "CRITICAL") assert_log(log, "", "critical message", "CRITICAL") -async def test_remove_older_logs(hass, simple_queue, hass_ws_client): +async def test_remove_older_logs(hass, hass_ws_client): """Test that older logs are rotated out.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message 1") _LOGGER.error("error message 2") _LOGGER.error("error message 3") - await _async_block_until_queue_empty(hass, simple_queue) - + await hass.async_block_till_done() log = await get_error_log(hass_ws_client) assert_log(log[0], "", "error message 3", "ERROR") assert_log(log[1], "", "error message 2", "ERROR") @@ -204,16 +213,14 @@ def log_msg(nr=2): _LOGGER.error("error message %s", nr) -async def test_dedupe_logs(hass, simple_queue, hass_ws_client): +async def test_dedupe_logs(hass, hass_ws_client): """Test that duplicate log entries are dedupe.""" - await async_setup_component(hass, system_log.DOMAIN, {}) + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message 1") log_msg() log_msg("2-2") _LOGGER.error("error message 3") - await _async_block_until_queue_empty(hass, simple_queue) log = await get_error_log(hass_ws_client) assert_log(log[0], "", "error message 3", "ERROR") @@ -221,8 +228,6 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): assert_log(log[1], "", ["error message 2", "error message 2-2"], "ERROR") log_msg() - await _async_block_until_queue_empty(hass, simple_queue) - log = await get_error_log(hass_ws_client) assert_log(log[0], "", ["error message 2", "error message 2-2"], "ERROR") assert log[0]["timestamp"] > log[0]["first_occurred"] @@ -231,7 +236,6 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): log_msg("2-4") log_msg("2-5") log_msg("2-6") - await _async_block_until_queue_empty(hass, simple_queue) log = await get_error_log(hass_ws_client) assert_log( @@ -248,17 +252,14 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): ) -async def test_clear_logs(hass, simple_queue, hass_ws_client): +async def test_clear_logs(hass, hass_ws_client): """Test that the log can be cleared via a service call.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) await hass.services.async_call(system_log.DOMAIN, system_log.SERVICE_CLEAR, {}) - await _async_block_until_queue_empty(hass, simple_queue) - + await hass.async_block_till_done() # Assert done by get_error_log await get_error_log(hass_ws_client) @@ -309,19 +310,17 @@ async def test_write_choose_level(hass): assert logger.method_calls[0] == ("debug", ("test_message",)) -async def test_unknown_path(hass, simple_queue, hass_ws_client): +async def test_unknown_path(hass, hass_ws_client): """Test error logged from unknown path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.findCaller = MagicMock(return_value=("unknown_path", 0, None, None)) _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["unknown_path", 0] -async def async_log_error_from_test_path(hass, path, sq): +async def async_log_error_from_test_path(hass, path, watcher): """Log error while mocking the path.""" call_path = "internal_path.py" with patch.object( @@ -337,34 +336,34 @@ async def async_log_error_from_test_path(hass, path, sq): ] ), ): + wait_empty = watcher.add_watcher("error message") _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, sq) + await wait_empty -async def test_homeassistant_path(hass, simple_queue, hass_ws_client): +async def test_homeassistant_path(hass, hass_ws_client): """Test error logged from Home Assistant path.""" - await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) - await hass.async_block_till_done() with patch( "homeassistant.components.system_log.HOMEASSISTANT_PATH", new=["venv_path/homeassistant"], ): + watcher = await async_setup_system_log(hass, BASIC_CONFIG) await async_log_error_from_test_path( - hass, "venv_path/homeassistant/component/component.py", simple_queue + hass, "venv_path/homeassistant/component/component.py", watcher ) log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["component/component.py", 5] -async def test_config_path(hass, simple_queue, hass_ws_client): +async def test_config_path(hass, hass_ws_client): """Test error logged from config path.""" - await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) - await hass.async_block_till_done() with patch.object(hass.config, "config_dir", new="config"): + watcher = await async_setup_system_log(hass, BASIC_CONFIG) + await async_log_error_from_test_path( - hass, "config/custom_component/test.py", simple_queue + hass, "config/custom_component/test.py", watcher ) log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["custom_component/test.py", 5] diff --git a/tests/conftest.py b/tests/conftest.py index 97b1a959d2c..4b2852e94fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,7 @@ from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password from homeassistant.components import mqtt, recorder +from homeassistant.components.network.models import Adapter, IPv4ConfiguredAddress from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, @@ -644,6 +645,25 @@ async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): yield _setup_mqtt_entry +@pytest.fixture(autouse=True) +def mock_network(): + """Mock network.""" + mock_adapter = Adapter( + name="eth0", + index=0, + enabled=True, + auto=True, + default=True, + ipv4=[IPv4ConfiguredAddress(address="10.10.10.10", network_prefix=24)], + ipv6=[], + ) + with patch( + "homeassistant.components.network.network.async_load_adapters", + return_value=[mock_adapter], + ): + yield + + @pytest.fixture(autouse=True) def mock_get_source_ip(): """Mock network util's async_get_source_ip.""" From c9df5888c2daab6d0287bf5f31cdf56dde3173b7 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 14 Jul 2022 05:09:27 -0400 Subject: [PATCH 425/820] Add Aladdin Connect wifi_rssi and battery_level sensors (#74258) --- .../components/aladdin_connect/__init__.py | 2 +- .../components/aladdin_connect/cover.py | 13 +- .../components/aladdin_connect/sensor.py | 115 ++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/sensor.py diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 1a28a03cf05..036f5364ef5 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -16,7 +16,7 @@ from .const import DOMAIN _LOGGER: Final = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.COVER] +PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 9c03cd322b6..d06ebebd076 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -24,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -87,8 +88,18 @@ class AladdinDevice(CoverEntity): self._device_id = device["device_id"] self._number = device["door_number"] - self._attr_name = device["name"] + self._name = device["name"] self._attr_unique_id = f"{self._device_id}-{self._number}" + self._attr_has_entity_name = True + + @property + def device_info(self) -> DeviceInfo | None: + """Device information for Aladdin Connect cover.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self._name, + manufacturer="Overhead Door", + ) async def async_added_to_hass(self) -> None: """Connect Aladdin Connect to the cloud.""" diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py new file mode 100644 index 00000000000..d9783d0f61d --- /dev/null +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -0,0 +1,115 @@ +"""Support for Aladdin Connect Garage Door sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast + +from AIOAladdinConnect import AladdinConnectClient + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .model import DoorDevice + + +@dataclass +class AccSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable + + +@dataclass +class AccSensorEntityDescription( + SensorEntityDescription, AccSensorEntityDescriptionMixin +): + """Describes AladdinConnect sensor entity.""" + + +SENSORS: tuple[AccSensorEntityDescription, ...] = ( + AccSensorEntityDescription( + key="battery_level", + name="Battery level", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=AladdinConnectClient.get_battery_status, + ), + AccSensorEntityDescription( + key="rssi", + name="Wi-Fi RSSI", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=AladdinConnectClient.get_rssi_status, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Aladdin Connect sensor devices.""" + + acc: AladdinConnectClient = hass.data[DOMAIN][entry.entry_id] + + entities = [] + doors = await acc.get_doors() + + for door in doors: + entities.extend( + [AladdinConnectSensor(acc, door, description) for description in SENSORS] + ) + + async_add_entities(entities) + + +class AladdinConnectSensor(SensorEntity): + """A sensor implementation for Aladdin Connect devices.""" + + _device: AladdinConnectSensor + entity_description: AccSensorEntityDescription + + def __init__( + self, + acc: AladdinConnectClient, + device: DoorDevice, + description: AccSensorEntityDescription, + ) -> None: + """Initialize a sensor for an Abode device.""" + self._device_id = device["device_id"] + self._number = device["door_number"] + self._name = device["name"] + self._acc = acc + self.entity_description = description + self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}" + self._attr_has_entity_name = True + + @property + def device_info(self) -> DeviceInfo | None: + """Device information for Aladdin Connect sensors.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self._name, + manufacturer="Overhead Door", + ) + + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + return cast( + float, + self.entity_description.value_fn(self._acc, self._device_id, self._number), + ) From 169264db66b97209c8b811d0d8fa70ae8f9cef7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Thu, 14 Jul 2022 11:21:01 +0200 Subject: [PATCH 426/820] Fix Blebox light scenes (#75106) * Bug fix for light platform, when async_turn_on recieves multiple keys. * Changes according to @MartinHjelmare suggestion. * Moved effect set call in BleBoxLightEntity.async_turn_on method. * Added tests for effect in light platform. Added ValueError raise if effect not in effect list. * Removed duplicated line from test as @MartinHjelmare suggested. --- homeassistant/components/blebox/light.py | 15 +++++--- tests/components/blebox/test_light.py | 46 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 2bb6bc91762..a2ad51cfc9c 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -159,15 +159,20 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): else: value = feature.apply_brightness(value, brightness) + try: + await self._feature.async_on(value) + except ValueError as exc: + raise ValueError( + f"Turning on '{self.name}' failed: Bad value {value}" + ) from exc + if effect is not None: - effect_value = self.effect_list.index(effect) - await self._feature.async_api_command("effect", effect_value) - else: try: - await self._feature.async_on(value) + effect_value = self.effect_list.index(effect) + await self._feature.async_api_command("effect", effect_value) except ValueError as exc: raise ValueError( - f"Turning on '{self.name}' failed: Bad value {value}" + f"Turning on with effect '{self.name}' failed: {effect} not in effect list." ) from exc async def async_turn_off(self, **kwargs): diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index f61496714fb..7afb78e5b03 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_EFFECT, ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ColorMode, @@ -524,3 +525,48 @@ async def test_turn_on_failure(feature, hass, config, caplog): assert f"Turning on '{feature_mock.full_name}' failed: Bad value 123" in str( info.value ) + + +async def test_wlightbox_on_effect(wlightbox, hass, config): + """Test light on.""" + + feature_mock, entity_id = wlightbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + def turn_on(value): + feature_mock.is_on = True + feature_mock.effect = "POLICE" + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + + with pytest.raises(ValueError) as info: + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_EFFECT: "NOT IN LIST"}, + blocking=True, + ) + + assert ( + f"Turning on with effect '{feature_mock.full_name}' failed: NOT IN LIST not in effect list." + in str(info.value) + ) + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_EFFECT: "POLICE"}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_EFFECT] == "POLICE" From 3bccac9949eadc4bcb088e0b12df2d7c88f2e7fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 11:37:59 +0200 Subject: [PATCH 427/820] Verisure config flow cleanups (#75144) --- .../components/verisure/config_flow.py | 37 +- tests/components/verisure/conftest.py | 50 +++ tests/components/verisure/test_config_flow.py | 411 +++++++++--------- 3 files changed, 263 insertions(+), 235 deletions(-) create mode 100644 tests/components/verisure/conftest.py diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 41687dbc6a4..119a9250736 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -34,8 +34,8 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): email: str entry: ConfigEntry - installations: dict[str, str] password: str + verisure: Verisure @staticmethod @callback @@ -50,11 +50,13 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - verisure = Verisure( + self.email = user_input[CONF_EMAIL] + self.password = user_input[CONF_PASSWORD] + self.verisure = Verisure( username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] ) try: - await self.hass.async_add_executor_job(verisure.login) + await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: LOGGER.debug("Could not log in to Verisure, %s", ex) errors["base"] = "invalid_auth" @@ -62,13 +64,6 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" else: - self.email = user_input[CONF_EMAIL] - self.password = user_input[CONF_PASSWORD] - self.installations = { - inst["giid"]: f"{inst['alias']} ({inst['street']})" - for inst in verisure.installations - } - return await self.async_step_installation() return self.async_show_form( @@ -86,22 +81,26 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Select Verisure installation to add.""" - if len(self.installations) == 1: - user_input = {CONF_GIID: list(self.installations)[0]} + installations = { + inst["giid"]: f"{inst['alias']} ({inst['street']})" + for inst in self.verisure.installations or [] + } if user_input is None: - return self.async_show_form( - step_id="installation", - data_schema=vol.Schema( - {vol.Required(CONF_GIID): vol.In(self.installations)} - ), - ) + if len(installations) != 1: + return self.async_show_form( + step_id="installation", + data_schema=vol.Schema( + {vol.Required(CONF_GIID): vol.In(installations)} + ), + ) + user_input = {CONF_GIID: list(installations)[0]} await self.async_set_unique_id(user_input[CONF_GIID]) self._abort_if_unique_id_configured() return self.async_create_entry( - title=self.installations[user_input[CONF_GIID]], + title=installations[user_input[CONF_GIID]], data={ CONF_EMAIL: self.email, CONF_PASSWORD: self.password, diff --git a/tests/components/verisure/conftest.py b/tests/components/verisure/conftest.py new file mode 100644 index 00000000000..f91215866d8 --- /dev/null +++ b/tests/components/verisure/conftest.py @@ -0,0 +1,50 @@ +"""Fixtures for Verisure integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.verisure.const import CONF_GIID, DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_GIID: "12345", + CONF_PASSWORD: "SuperS3cr3t!", + }, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.verisure.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_verisure_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Tailscale client.""" + with patch( + "homeassistant.components.verisure.config_flow.Verisure", autospec=True + ) as verisure_mock: + verisure = verisure_mock.return_value + verisure.login.return_value = True + verisure.installations = [ + {"giid": "12345", "alias": "ascending", "street": "12345th street"}, + {"giid": "54321", "alias": "descending", "street": "54321th street"}, + ] + yield verisure diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 03fa0fa82cc..d957709c878 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Verisure config flow.""" from __future__ import annotations -from unittest.mock import PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from verisure import Error as VerisureError, LoginError as VerisureLoginError @@ -21,151 +21,151 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -TEST_INSTALLATIONS = [ - {"giid": "12345", "alias": "ascending", "street": "12345th street"}, - {"giid": "54321", "alias": "descending", "street": "54321th street"}, -] -TEST_INSTALLATION = [TEST_INSTALLATIONS[0]] - -async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: +async def test_full_user_flow_single_installation( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: """Test a full user initiated configuration flow with a single installation.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATION - ) - mock_verisure.login.return_value = True + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "ascending (12345th street)" - assert result2["data"] == { + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("title") == "ascending (12345th street)" + assert result2.get("data") == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> None: +async def test_full_user_flow_multiple_installations( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: """Test a full user initiated configuration flow with multiple installations.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATIONS - ) - mock_verisure.login.return_value = True + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + assert result2.get("step_id") == "installation" + assert result2.get("type") == FlowResultType.FORM + assert result2.get("errors") is None + assert "flow_id" in result2 - assert result2["step_id"] == "installation" - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] is None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {"giid": "54321"} + ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], {"giid": "54321"} - ) - await hass.async_block_till_done() - - assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "descending (54321th street)" - assert result3["data"] == { + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "descending (54321th street)" + assert result3.get("data") == { CONF_GIID: "54321", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_invalid_login(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "side_effect,error", + [ + (VerisureLoginError, "invalid_auth"), + (VerisureError, "unknown"), + ], +) +async def test_verisure_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + side_effect: Exception, + error: str, +) -> None: """Test a flow with an invalid Verisure My Pages login.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureLoginError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + assert "flow_id" in result - assert result2["type"] == FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_unknown_error(hass: HomeAssistant) -> None: - """Test a flow with an invalid Verisure My Pages login.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + mock_verisure_config_flow.login.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "user" + assert result2.get("errors") == {"base": error} + assert "flow_id" in result2 - assert result2["type"] == FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + mock_verisure_config_flow.login.side_effect = None + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "ascending (12345th street)" + assert result3.get("data") == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_setup_entry.mock_calls) == 1 async def test_dhcp(hass: HomeAssistant) -> None: @@ -178,144 +178,121 @@ async def test_dhcp(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" -async def test_reauth_flow(hass: HomeAssistant) -> None: +async def test_reauth_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: """Test a reauthentication flow.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, }, - data=entry.data, + data=mock_config_entry.data, ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} + assert result.get("step_id") == "reauth_confirm" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - return_value=True, - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "correct horse battery staple", - }, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple", + }, + ) + await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" - assert entry.data == { + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "reauth_successful" + assert mock_config_entry.data == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "correct horse battery staple", } - assert len(mock_verisure.mock_calls) == 1 + assert len(mock_verisure_config_flow.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "side_effect,error", + [ + (VerisureLoginError, "invalid_auth"), + (VerisureError, "unknown"), + ], +) +async def test_reauth_flow_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + error: str, +) -> None: """Test a reauthentication flow.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, }, - data=entry.data, + data=mock_config_entry.data, ) - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureLoginError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "WrOngP4ssw0rd!", - }, - ) - await hass.async_block_till_done() + assert "flow_id" in result - assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_reauth_flow_unknown_error(hass: HomeAssistant) -> None: - """Test a reauthentication flow, with an unknown error happening.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_PASSWORD: "SuperS3cr3t!", + mock_verisure_config_flow.login.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "WrOngP4ssw0rd!", }, ) - entry.add_to_hass(hass) + await hass.async_block_till_done() - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, + assert result2.get("step_id") == "reauth_confirm" + assert result2.get("type") == FlowResultType.FORM + assert result2.get("errors") == {"base": error} + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple", }, - data=entry.data, ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "WrOngP4ssw0rd!", - }, - ) - await hass.async_block_till_done() + assert mock_config_entry.data == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "correct horse battery staple", + } - assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( @@ -362,16 +339,17 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"] == output + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("data") == output async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: @@ -392,9 +370,10 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] == {} + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert result.get("errors") == {} + assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -404,6 +383,6 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] == {"base": "code_format_mismatch"} + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert result.get("errors") == {"base": "code_format_mismatch"} From 51c17197c53ac633de4cfd9569e17bde7acc1f2c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 11:40:10 +0200 Subject: [PATCH 428/820] Remove nzbget from mypy ignore list (#75158) --- homeassistant/components/nzbget/coordinator.py | 12 ++++++++++-- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index 5851bb21b41..c037619d31b 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -1,6 +1,8 @@ """Provides the NZBGet DataUpdateCoordinator.""" +from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any from async_timeout import timeout from pynzbgetapi import NZBGetAPI, NZBGetAPIException @@ -25,7 +27,13 @@ _LOGGER = logging.getLogger(__name__) class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching NZBGet data.""" - def __init__(self, hass: HomeAssistant, *, config: dict, options: dict) -> None: + def __init__( + self, + hass: HomeAssistant, + *, + config: Mapping[str, Any], + options: Mapping[str, Any], + ) -> None: """Initialize global NZBGet data updater.""" self.nzbget = NZBGetAPI( config[CONF_HOST], @@ -37,7 +45,7 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): ) self._completed_downloads_init = False - self._completed_downloads = {} + self._completed_downloads = set[tuple]() update_interval = timedelta(seconds=options[CONF_SCAN_INTERVAL]) diff --git a/mypy.ini b/mypy.ini index b973b959213..1e6741eccd1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2689,18 +2689,6 @@ ignore_errors = true [mypy-homeassistant.components.minecraft_server.sensor] ignore_errors = true -[mypy-homeassistant.components.nzbget] -ignore_errors = true - -[mypy-homeassistant.components.nzbget.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.nzbget.coordinator] -ignore_errors = true - -[mypy-homeassistant.components.nzbget.switch] -ignore_errors = true - [mypy-homeassistant.components.onvif.base] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 87ace51a3d6..64260b90033 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -27,10 +27,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", - "homeassistant.components.nzbget", - "homeassistant.components.nzbget.config_flow", - "homeassistant.components.nzbget.coordinator", - "homeassistant.components.nzbget.switch", "homeassistant.components.onvif.base", "homeassistant.components.onvif.binary_sensor", "homeassistant.components.onvif.camera", From b60f6c7cdd4d51ca9f695667be6b9bac70dbebc0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 11:50:21 +0200 Subject: [PATCH 429/820] Bump pip_check (#75164) --- .github/workflows/ci.yaml | 4 ++-- script/pip_check | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 573445a4d2e..29527383bab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,8 +20,8 @@ on: type: boolean env: - CACHE_VERSION: 0 - PIP_CACHE_VERSION: 0 + CACHE_VERSION: 1 + PIP_CACHE_VERSION: 1 HA_SHORT_VERSION: 2022.8 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit diff --git a/script/pip_check b/script/pip_check index f29ea5a5dd0..6f48ce15bb1 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=6 +DEPENDENCY_CONFLICTS=7 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From debd475a6dca085535f758369a3e4e3cb6efa7c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 11:55:43 +0200 Subject: [PATCH 430/820] Remove onvif from mypy ignore list (#75162) --- homeassistant/components/onvif/base.py | 34 ++++++++++++------- .../components/onvif/binary_sensor.py | 8 +++-- homeassistant/components/onvif/button.py | 6 ++-- homeassistant/components/onvif/camera.py | 17 ++++++---- homeassistant/components/onvif/device.py | 24 ++++++++----- homeassistant/components/onvif/sensor.py | 6 ++-- mypy.ini | 15 -------- script/hassfest/mypy_config.py | 5 --- 8 files changed, 56 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/onvif/base.py b/homeassistant/components/onvif/base.py index 93fd6a26b35..f40b6173a3b 100644 --- a/homeassistant/components/onvif/base.py +++ b/homeassistant/components/onvif/base.py @@ -1,42 +1,50 @@ """Base classes for ONVIF entities.""" +from __future__ import annotations + from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .device import ONVIFDevice -from .models import Profile class ONVIFBaseEntity(Entity): """Base class common to all ONVIF entities.""" - def __init__(self, device: ONVIFDevice, profile: Profile = None) -> None: + def __init__(self, device: ONVIFDevice) -> None: """Initialize the ONVIF entity.""" self.device: ONVIFDevice = device - self.profile: Profile = profile @property def available(self): """Return True if device is available.""" return self.device.available + @property + def mac_or_serial(self) -> str: + """Return MAC or serial, for unique_id generation. + + MAC address is not always available, and given the number + of non-conformant ONVIF devices we have historically supported, + we can not guarantee serial number either. Due to this, we have + adopted an either/or approach in the config entry setup, and can + guarantee that one or the other will be populated. + See: https://github.com/home-assistant/core/issues/35883 + """ + return ( + self.device.info.mac + or self.device.info.serial_number # type:ignore[return-value] + ) + @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - connections = None + connections: set[tuple[str, str]] = set() if self.device.info.mac: connections = {(CONNECTION_NETWORK_MAC, self.device.info.mac)} return DeviceInfo( connections=connections, - identifiers={ - # MAC address is not always available, and given the number - # of non-conformant ONVIF devices we have historically supported, - # we can not guarantee serial number either. Due to this, we have - # adopted an either/or approach in the config entry setup, and can - # guarantee that one or the other will be populated. - # See: https://github.com/home-assistant/core/issues/35883 - (DOMAIN, self.device.info.mac or self.device.info.serial_number) - }, + identifiers={(DOMAIN, self.mac_or_serial)}, manufacturer=self.device.info.manufacturer, model=self.device.info.model, name=self.device.name, diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py index 2e72d331d3d..cd9af1d83b5 100644 --- a/homeassistant/components/onvif/binary_sensor.py +++ b/homeassistant/components/onvif/binary_sensor.py @@ -48,15 +48,16 @@ async def async_setup_entry( device.events.async_add_listener(async_check_entities) - return True - class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity): """Representation of a binary ONVIF event.""" _attr_should_poll = False + _attr_unique_id: str - def __init__(self, uid, device: ONVIFDevice, entry: er.RegistryEntry | None = None): + def __init__( + self, uid: str, device: ONVIFDevice, entry: er.RegistryEntry | None = None + ) -> None: """Initialize the ONVIF binary sensor.""" self._attr_unique_id = uid if entry is not None: @@ -65,6 +66,7 @@ class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity): self._attr_name = entry.name else: event = device.events.get_uid(uid) + assert event self._attr_device_class = event.device_class self._attr_entity_category = event.entity_category self._attr_entity_registry_enabled_default = event.entity_enabled diff --git a/homeassistant/components/onvif/button.py b/homeassistant/components/onvif/button.py index 0af4a16d269..2732c672ad9 100644 --- a/homeassistant/components/onvif/button.py +++ b/homeassistant/components/onvif/button.py @@ -30,9 +30,7 @@ class RebootButton(ONVIFBaseEntity, ButtonEntity): """Initialize the button entity.""" super().__init__(device) self._attr_name = f"{self.device.name} Reboot" - self._attr_unique_id = ( - f"{self.device.info.mac or self.device.info.serial_number}_reboot" - ) + self._attr_unique_id = f"{self.mac_or_serial}_reboot" async def async_press(self) -> None: """Send out a SystemReboot command.""" @@ -49,7 +47,7 @@ class SetSystemDateAndTimeButton(ONVIFBaseEntity, ButtonEntity): """Initialize the button entity.""" super().__init__(device) self._attr_name = f"{self.device.name} Set System Date and Time" - self._attr_unique_id = f"{self.device.info.mac or self.device.info.serial_number}_setsystemdatetime" + self._attr_unique_id = f"{self.mac_or_serial}_setsystemdatetime" async def async_press(self) -> None: """Send out a SetSystemDateAndTime command.""" diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 6aa7ae42767..5aa49f68aa6 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -13,6 +13,7 @@ from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, ) +from homeassistant.components.stream.const import RTSP_TRANSPORTS from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.core import HomeAssistant @@ -46,6 +47,8 @@ from .const import ( ZOOM_IN, ZOOM_OUT, ) +from .device import ONVIFDevice +from .models import Profile async def async_setup_entry( @@ -85,20 +88,19 @@ async def async_setup_entry( [ONVIFCameraEntity(device, profile) for profile in device.profiles] ) - return True - class ONVIFCameraEntity(ONVIFBaseEntity, Camera): """Representation of an ONVIF camera.""" _attr_supported_features = CameraEntityFeature.STREAM - def __init__(self, device, profile): + def __init__(self, device: ONVIFDevice, profile: Profile) -> None: """Initialize ONVIF camera entity.""" - ONVIFBaseEntity.__init__(self, device, profile) + ONVIFBaseEntity.__init__(self, device) Camera.__init__(self) + self.profile = profile self.stream_options[CONF_RTSP_TRANSPORT] = device.config_entry.options.get( - CONF_RTSP_TRANSPORT + CONF_RTSP_TRANSPORT, next(iter(RTSP_TRANSPORTS)) ) self.stream_options[ CONF_USE_WALLCLOCK_AS_TIMESTAMPS @@ -118,8 +120,8 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): def unique_id(self) -> str: """Return a unique ID.""" if self.profile.index: - return f"{self.device.info.mac or self.device.info.serial_number}_{self.profile.index}" - return self.device.info.mac or self.device.info.serial_number + return f"{self.mac_or_serial}_{self.profile.index}" + return self.mac_or_serial @property def entity_registry_enabled_default(self) -> bool: @@ -149,6 +151,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): ) if image is None: + assert self._stream_uri return await ffmpeg.async_get_image( self.hass, self._stream_uri, diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 5907ea90124..dda28e07a2a 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -42,21 +42,21 @@ from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video class ONVIFDevice: """Manages an ONVIF device.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None) -> None: + device: ONVIFCamera + events: EventManager + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the device.""" self.hass: HomeAssistant = hass self.config_entry: ConfigEntry = config_entry self.available: bool = True - self.device: ONVIFCamera = None - self.events: EventManager = None - self.info: DeviceInfo = DeviceInfo() self.capabilities: Capabilities = Capabilities() self.profiles: list[Profile] = [] self.max_resolution: int = 0 - self._dt_diff_seconds: int = 0 + self._dt_diff_seconds: float = 0 @property def name(self) -> str: @@ -99,6 +99,7 @@ class ONVIFDevice: await self.async_check_date_and_time() # Create event manager + assert self.config_entry.unique_id self.events = EventManager( self.hass, self.device, self.config_entry.unique_id ) @@ -297,7 +298,7 @@ class ONVIFDevice: """Obtain media profiles for this device.""" media_service = self.device.create_media_service() result = await media_service.GetProfiles() - profiles = [] + profiles: list[Profile] = [] if not isinstance(result, list): return profiles @@ -396,7 +397,7 @@ class ONVIFDevice: req.ProfileToken = profile.token if move_mode == CONTINUOUS_MOVE: # Guard against unsupported operation - if not profile.ptz.continuous: + if not profile.ptz or not profile.ptz.continuous: LOGGER.warning( "ContinuousMove not supported on device '%s'", self.name ) @@ -419,7 +420,7 @@ class ONVIFDevice: ) elif move_mode == RELATIVE_MOVE: # Guard against unsupported operation - if not profile.ptz.relative: + if not profile.ptz or not profile.ptz.relative: LOGGER.warning( "RelativeMove not supported on device '%s'", self.name ) @@ -436,7 +437,7 @@ class ONVIFDevice: await ptz_service.RelativeMove(req) elif move_mode == ABSOLUTE_MOVE: # Guard against unsupported operation - if not profile.ptz.absolute: + if not profile.ptz or not profile.ptz.absolute: LOGGER.warning( "AbsoluteMove not supported on device '%s'", self.name ) @@ -453,6 +454,11 @@ class ONVIFDevice: await ptz_service.AbsoluteMove(req) elif move_mode == GOTOPRESET_MOVE: # Guard against unsupported operation + if not profile.ptz or not profile.ptz.presets: + LOGGER.warning( + "Absolute Presets not supported on device '%s'", self.name + ) + return if preset_val not in profile.ptz.presets: LOGGER.warning( "PTZ preset '%s' does not exist on device '%s'. Available Presets: %s", diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index 67c0ee3da3f..b662dca1d5d 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import date, datetime +from decimal import Decimal from homeassistant.components.sensor import RestoreSensor from homeassistant.config_entries import ConfigEntry @@ -47,8 +48,6 @@ async def async_setup_entry( device.events.async_add_listener(async_check_entities) - return True - class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): """Representation of a ONVIF sensor event.""" @@ -65,6 +64,7 @@ class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): self._attr_native_unit_of_measurement = entry.unit_of_measurement else: event = device.events.get_uid(uid) + assert event self._attr_device_class = event.device_class self._attr_entity_category = event.entity_category self._attr_entity_registry_enabled_default = event.entity_enabled @@ -75,7 +75,7 @@ class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): super().__init__(device) @property - def native_value(self) -> StateType | date | datetime: + def native_value(self) -> StateType | date | datetime | Decimal: """Return the value reported by the sensor.""" if (event := self.device.events.get_uid(self._attr_unique_id)) is not None: return event.value diff --git a/mypy.ini b/mypy.ini index 1e6741eccd1..8588209b87a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2689,21 +2689,6 @@ ignore_errors = true [mypy-homeassistant.components.minecraft_server.sensor] ignore_errors = true -[mypy-homeassistant.components.onvif.base] -ignore_errors = true - -[mypy-homeassistant.components.onvif.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.onvif.camera] -ignore_errors = true - -[mypy-homeassistant.components.onvif.device] -ignore_errors = true - -[mypy-homeassistant.components.onvif.sensor] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 64260b90033..992d8836916 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -27,11 +27,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", - "homeassistant.components.onvif.base", - "homeassistant.components.onvif.binary_sensor", - "homeassistant.components.onvif.camera", - "homeassistant.components.onvif.device", - "homeassistant.components.onvif.sensor", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 28c082a0804710e24a92ae009a371eb11336fb8e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 12:42:18 +0200 Subject: [PATCH 431/820] Update sentry-sdk to 1.7.1 (#75154) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 44bcd8025bb..29b897e4a3c 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.7.0"], + "requirements": ["sentry-sdk==1.7.1"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 380a8d24f21..f93afa0debd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.7.0 +sentry-sdk==1.7.1 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5469672ee54..a1959df8456 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1434,7 +1434,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.7.0 +sentry-sdk==1.7.1 # homeassistant.components.sharkiq sharkiq==0.0.1 From c260413e2a344256e8328e498ab45fc179c29cd6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:08:50 +0200 Subject: [PATCH 432/820] Remove lovelace from mypy ignore list (#75167) * Remove lovelace from mypy ignore list * Raise error on failed config --- homeassistant/components/lovelace/__init__.py | 8 ++++++-- homeassistant/components/lovelace/resources.py | 4 ++-- homeassistant/components/lovelace/websocket.py | 8 ++++---- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 5 files changed, 12 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 9b268dfdfcd..cf74a3a588c 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -86,11 +86,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config = await async_process_component_config(hass, conf, integration) + if config is None: + raise HomeAssistantError("Config validation failed") + resource_collection = await create_yaml_resource_col( hass, config[DOMAIN].get(CONF_RESOURCES) ) hass.data[DOMAIN]["resources"] = resource_collection + default_config: dashboard.LovelaceConfig if mode == MODE_YAML: default_config = dashboard.LovelaceYAML(hass, None, None) resource_collection = await create_yaml_resource_col(hass, yaml_resources) @@ -179,8 +183,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Process YAML dashboards for url_path, dashboard_conf in hass.data[DOMAIN]["yaml_dashboards"].items(): # For now always mode=yaml - config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) - hass.data[DOMAIN]["dashboards"][url_path] = config + lovelace_config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) + hass.data[DOMAIN]["dashboards"][url_path] = lovelace_config try: _register_panel(hass, url_path, MODE_YAML, dashboard_conf, False) diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 22297c54d6c..7e7c670baf5 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -7,7 +7,7 @@ import uuid import voluptuous as vol -from homeassistant.const import CONF_RESOURCES, CONF_TYPE +from homeassistant.const import CONF_ID, CONF_RESOURCES, CONF_TYPE from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, storage @@ -94,7 +94,7 @@ class ResourceStorageCollection(collection.StorageCollection): conf.pop(CONF_RESOURCES) for item in data: - item[collection.CONF_ID] = uuid.uuid4().hex + item[CONF_ID] = uuid.uuid4().hex data = {"items": data} diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index 45a042c1f2e..cb45d9cfbc6 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -45,8 +45,8 @@ def _handle_errors(func): return send_with_error_handling -@websocket_api.async_response @websocket_api.websocket_command({"type": "lovelace/resources"}) +@websocket_api.async_response async def websocket_lovelace_resources(hass, connection, msg): """Send Lovelace UI resources over WebSocket configuration.""" resources = hass.data[DOMAIN]["resources"] @@ -58,7 +58,6 @@ async def websocket_lovelace_resources(hass, connection, msg): connection.send_result(msg["id"], resources.async_items()) -@websocket_api.async_response @websocket_api.websocket_command( { "type": "lovelace/config", @@ -66,6 +65,7 @@ async def websocket_lovelace_resources(hass, connection, msg): vol.Optional(CONF_URL_PATH): vol.Any(None, cv.string), } ) +@websocket_api.async_response @_handle_errors async def websocket_lovelace_config(hass, connection, msg, config): """Send Lovelace UI config over WebSocket configuration.""" @@ -73,7 +73,6 @@ async def websocket_lovelace_config(hass, connection, msg, config): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { "type": "lovelace/config/save", @@ -81,6 +80,7 @@ async def websocket_lovelace_config(hass, connection, msg, config): vol.Optional(CONF_URL_PATH): vol.Any(None, cv.string), } ) +@websocket_api.async_response @_handle_errors async def websocket_lovelace_save_config(hass, connection, msg, config): """Save Lovelace UI configuration.""" @@ -88,13 +88,13 @@ async def websocket_lovelace_save_config(hass, connection, msg, config): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { "type": "lovelace/config/delete", vol.Optional(CONF_URL_PATH): vol.Any(None, cv.string), } ) +@websocket_api.async_response @_handle_errors async def websocket_lovelace_delete_config(hass, connection, msg, config): """Delete Lovelace UI configuration.""" diff --git a/mypy.ini b/mypy.ini index 8588209b87a..93ae6c2f78c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2668,18 +2668,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.climate] ignore_errors = true -[mypy-homeassistant.components.lovelace] -ignore_errors = true - -[mypy-homeassistant.components.lovelace.dashboard] -ignore_errors = true - -[mypy-homeassistant.components.lovelace.resources] -ignore_errors = true - -[mypy-homeassistant.components.lovelace.websocket] -ignore_errors = true - [mypy-homeassistant.components.minecraft_server] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 992d8836916..3b8e80490b6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -20,10 +20,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.http_api", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", - "homeassistant.components.lovelace", - "homeassistant.components.lovelace.dashboard", - "homeassistant.components.lovelace.resources", - "homeassistant.components.lovelace.websocket", "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", From e16bd1e47167f705177feb22f8ac2072682f621d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:46:37 +0200 Subject: [PATCH 433/820] Remove evohome climate from mypy ignore list (#75169) * Remove evohome climate from mypy ignore list * Raise error --- homeassistant/components/evohome/__init__.py | 4 +-- homeassistant/components/evohome/climate.py | 33 ++++++++------------ mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 1bb84dfc40a..27c3698143b 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -540,11 +540,11 @@ class EvoDevice(Entity): return await self.async_tcs_svc_request(payload["service"], payload["data"]) - async def async_tcs_svc_request(self, service: dict, data: dict) -> None: + async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (system mode) for a controller.""" raise NotImplementedError - async def async_zone_svc_request(self, service: dict, data: dict) -> None: + async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (setpoint override) for a zone.""" raise NotImplementedError diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 700fa6ab078..841619af6f1 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime as dt import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -15,6 +16,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -93,9 +95,8 @@ async def async_setup_platform( broker.params[CONF_LOCATION_IDX], ) - controller = EvoController(broker, broker.tcs) + entities: list[EvoClimateEntity] = [EvoController(broker, broker.tcs)] - zones = [] for zone in broker.tcs.zones.values(): if zone.modelType == "HeatingZone" or zone.zoneType == "Thermostat": _LOGGER.debug( @@ -107,7 +108,7 @@ async def async_setup_platform( ) new_entity = EvoZone(broker, zone) - zones.append(new_entity) + entities.append(new_entity) else: _LOGGER.warning( @@ -119,7 +120,7 @@ async def async_setup_platform( zone.name, ) - async_add_entities([controller] + zones, update_before_add=True) + async_add_entities(entities, update_before_add=True) class EvoClimateEntity(EvoDevice, ClimateEntity): @@ -168,7 +169,7 @@ class EvoZone(EvoChild, EvoClimateEntity): ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) - async def async_zone_svc_request(self, service: dict, data: dict) -> None: + async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (setpoint override) for a zone.""" if service == SVC_RESET_ZONE_OVERRIDE: await self._evo_broker.call_client_api( @@ -272,7 +273,7 @@ class EvoZone(EvoChild, EvoClimateEntity): self._evo_device.cancel_temp_override() ) - async def async_set_preset_mode(self, preset_mode: str | None) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode; if None, then revert to following the schedule.""" evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW) @@ -332,7 +333,7 @@ class EvoController(EvoClimateEntity): ClimateEntityFeature.PRESET_MODE if self._attr_preset_modes else 0 ) - async def async_tcs_svc_request(self, service: dict, data: dict) -> None: + async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (system mode) for a controller. Data validation is not required, it will have been done upstream. @@ -385,25 +386,17 @@ class EvoController(EvoClimateEntity): """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - @property - def min_temp(self) -> float: - """Return None as Controllers don't have a target temperature.""" - return None - - @property - def max_temp(self) -> float: - """Return None as Controllers don't have a target temperature.""" - return None - async def async_set_temperature(self, **kwargs) -> None: """Raise exception as Controllers don't have a target temperature.""" raise NotImplementedError("Evohome Controllers don't have target temperatures.") - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set an operating mode for a Controller.""" - await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) + if not (tcs_mode := HA_HVAC_TO_TCS.get(hvac_mode)): + raise HomeAssistantError(f"Invalid hvac_mode: {hvac_mode}") + await self._set_tcs_mode(tcs_mode) - async def async_set_preset_mode(self, preset_mode: str | None) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode; if None, then revert to 'Auto' mode.""" await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) diff --git a/mypy.ini b/mypy.ini index 93ae6c2f78c..2297869dbb0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2665,9 +2665,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome] ignore_errors = true -[mypy-homeassistant.components.evohome.climate] -ignore_errors = true - [mypy-homeassistant.components.minecraft_server] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3b8e80490b6..f53d6a6e6b0 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -19,7 +19,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", "homeassistant.components.evohome", - "homeassistant.components.evohome.climate", "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", From 20432ccc7601f46ce0f84f5b539ada2d9b5b3381 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 14 Jul 2022 07:02:13 -0500 Subject: [PATCH 434/820] Migrate roku to new entity naming (#74819) * migrate roku to new entity naming * Update binary_sensor.py * Update sensor.py * Update test_binary_sensor.py * Update sensor.py * Update entity.py * Update media_player.py * Update remote.py * Update media_player.py * Update remote.py * Update entity.py * Update entity.py * Update entity.py * Update entity.py --- .../components/roku/binary_sensor.py | 6 +-- homeassistant/components/roku/entity.py | 52 ++++++++++--------- homeassistant/components/roku/media_player.py | 22 ++++---- homeassistant/components/roku/remote.py | 22 ++++---- tests/components/roku/test_binary_sensor.py | 12 ++--- 5 files changed, 55 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/roku/binary_sensor.py b/homeassistant/components/roku/binary_sensor.py index 5b6da073dd1..243ea994dfa 100644 --- a/homeassistant/components/roku/binary_sensor.py +++ b/homeassistant/components/roku/binary_sensor.py @@ -36,7 +36,7 @@ class RokuBinarySensorEntityDescription( BINARY_SENSORS: tuple[RokuBinarySensorEntityDescription, ...] = ( RokuBinarySensorEntityDescription( key="headphones_connected", - name="Headphones Connected", + name="Headphones connected", icon="mdi:headphones", value_fn=lambda device: device.info.headphones_connected, ), @@ -49,14 +49,14 @@ BINARY_SENSORS: tuple[RokuBinarySensorEntityDescription, ...] = ( ), RokuBinarySensorEntityDescription( key="supports_ethernet", - name="Supports Ethernet", + name="Supports ethernet", icon="mdi:ethernet", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.ethernet_support, ), RokuBinarySensorEntityDescription( key="supports_find_remote", - name="Supports Find Remote", + name="Supports find remote", icon="mdi:remote", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.supports_find_remote, diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index 39373c96c6a..a85024f8220 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -25,30 +25,32 @@ class RokuEntity(CoordinatorEntity[RokuDataUpdateCoordinator]): if description is not None: self.entity_description = description - self._attr_name = f"{coordinator.data.info.name} {description.name}" - if device_id is not None: + + if device_id is None: + self._attr_name = f"{coordinator.data.info.name} {description.name}" + + if device_id is not None: + self._attr_has_entity_name = True + + if description is not None: self._attr_unique_id = f"{device_id}_{description.key}" + else: + self._attr_unique_id = device_id - @property - def device_info(self) -> DeviceInfo | None: - """Return device information about this Roku device.""" - if self._device_id is None: - return None - - return DeviceInfo( - identifiers={(DOMAIN, self._device_id)}, - connections={ - (CONNECTION_NETWORK_MAC, mac_address) - for mac_address in ( - self.coordinator.data.info.wifi_mac, - self.coordinator.data.info.ethernet_mac, - ) - if mac_address is not None - }, - name=self.coordinator.data.info.name, - manufacturer=self.coordinator.data.info.brand, - model=self.coordinator.data.info.model_name, - hw_version=self.coordinator.data.info.model_number, - sw_version=self.coordinator.data.info.version, - suggested_area=self.coordinator.data.info.device_location, - ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + connections={ + (CONNECTION_NETWORK_MAC, mac_address) + for mac_address in ( + self.coordinator.data.info.wifi_mac, + self.coordinator.data.info.ethernet_mac, + ) + if mac_address is not None + }, + name=self.coordinator.data.info.name, + manufacturer=self.coordinator.data.info.brand, + model=self.coordinator.data.info.model_name, + hw_version=self.coordinator.data.info.model_number, + sw_version=self.coordinator.data.info.version, + suggested_area=self.coordinator.data.info.device_location, + ) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index a47432694dd..d7b9a3489c9 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -99,7 +99,15 @@ async def async_setup_entry( """Set up the Roku config entry.""" coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] unique_id = coordinator.data.info.serial_number - async_add_entities([RokuMediaPlayer(unique_id, coordinator)], True) + async_add_entities( + [ + RokuMediaPlayer( + device_id=unique_id, + coordinator=coordinator, + ) + ], + True, + ) platform = entity_platform.async_get_current_platform() @@ -127,18 +135,6 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) - def __init__( - self, unique_id: str | None, coordinator: RokuDataUpdateCoordinator - ) -> None: - """Initialize the Roku device.""" - super().__init__( - coordinator=coordinator, - device_id=unique_id, - ) - - self._attr_name = coordinator.data.info.name - self._attr_unique_id = unique_id - def _media_playback_trackable(self) -> bool: """Detect if we have enough media data to track playback.""" if self.coordinator.data.media is None or self.coordinator.data.media.live: diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 6d1312c0b03..fceac67a477 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -21,24 +21,22 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Load Roku remote based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] unique_id = coordinator.data.info.serial_number - async_add_entities([RokuRemote(unique_id, coordinator)], True) + async_add_entities( + [ + RokuRemote( + device_id=unique_id, + coordinator=coordinator, + ) + ], + True, + ) class RokuRemote(RokuEntity, RemoteEntity): """Device that sends commands to an Roku.""" - def __init__(self, unique_id: str, coordinator: RokuDataUpdateCoordinator) -> None: - """Initialize the Roku device.""" - super().__init__( - device_id=unique_id, - coordinator=coordinator, - ) - - self._attr_name = coordinator.data.info.name - self._attr_unique_id = unique_id - @property def is_on(self) -> bool: """Return true if device is on.""" diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py index 24f92b0b11b..706b1eceddb 100644 --- a/tests/components/roku/test_binary_sensor.py +++ b/tests/components/roku/test_binary_sensor.py @@ -29,7 +29,7 @@ async def test_roku_binary_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_headphones_connected" assert entry.entity_category is None assert state.state == STATE_OFF - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Headphones Connected" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Headphones connected" assert state.attributes.get(ATTR_ICON) == "mdi:headphones" assert ATTR_DEVICE_CLASS not in state.attributes @@ -51,7 +51,7 @@ async def test_roku_binary_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_supports_ethernet" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_ON - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports Ethernet" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports ethernet" assert state.attributes.get(ATTR_ICON) == "mdi:ethernet" assert ATTR_DEVICE_CLASS not in state.attributes @@ -62,7 +62,7 @@ async def test_roku_binary_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_supports_find_remote" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_OFF - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports Find Remote" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports find remote" assert state.attributes.get(ATTR_ICON) == "mdi:remote" assert ATTR_DEVICE_CLASS not in state.attributes @@ -105,7 +105,7 @@ async def test_rokutv_binary_sensors( assert state.state == STATE_OFF assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == '58" Onn Roku TV Headphones Connected' + == '58" Onn Roku TV Headphones connected' ) assert state.attributes.get(ATTR_ICON) == "mdi:headphones" assert ATTR_DEVICE_CLASS not in state.attributes @@ -131,7 +131,7 @@ async def test_rokutv_binary_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_ON assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports Ethernet' + state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports ethernet' ) assert state.attributes.get(ATTR_ICON) == "mdi:ethernet" assert ATTR_DEVICE_CLASS not in state.attributes @@ -147,7 +147,7 @@ async def test_rokutv_binary_sensors( assert state.state == STATE_ON assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == '58" Onn Roku TV Supports Find Remote' + == '58" Onn Roku TV Supports find remote' ) assert state.attributes.get(ATTR_ICON) == "mdi:remote" assert ATTR_DEVICE_CLASS not in state.attributes From a3c1926da57b5c89bf77c15622d75d231653f5d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 14:40:17 +0200 Subject: [PATCH 435/820] Add mock_bluetooth fixture (#75075) --- tests/components/bluetooth/conftest.py | 24 ----------------- tests/components/bluetooth/test_init.py | 23 +--------------- tests/conftest.py | 36 ++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 47 deletions(-) diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index fc0bd85b795..760500fe7a1 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1,25 +1 @@ """Tests for the bluetooth component.""" - -import threading - -import pytest - -from tests.common import INSTANCES - - -@pytest.fixture(autouse=True) -def verify_cleanup(): - """Verify that the test has cleaned up resources correctly.""" - threads_before = frozenset(threading.enumerate()) - - yield - - if len(INSTANCES) >= 2: - count = len(INSTANCES) - for inst in INSTANCES: - inst.stop() - pytest.exit(f"Detected non stopped instances ({count}), aborting test run") - - threads = frozenset(threading.enumerate()) - threads_before - for thread in threads: - assert isinstance(thread, threading._DummyThread) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index f43ef4737f6..2bbe4ce7dcb 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,10 +1,8 @@ """Tests for the Bluetooth integration.""" -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch -import bleak from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice -import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( @@ -16,25 +14,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT from homeassistant.setup import async_setup_component -@pytest.fixture() -def mock_bleak_scanner_start(): - """Fixture to mock starting the bleak scanner.""" - scanner = bleak.BleakScanner - models.HA_BLEAK_SCANNER = None - - with patch("homeassistant.components.bluetooth.HaBleakScanner.stop"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", - ) as mock_bleak_scanner_start: - yield mock_bleak_scanner_start - - # We need to drop the stop method from the object since we patched - # out start and this fixture will expire before the stop method is called - # when EVENT_HOMEASSISTANT_STOP is fired. - if models.HA_BLEAK_SCANNER: - models.HA_BLEAK_SCANNER.stop = AsyncMock() - bleak.BleakScanner = scanner - - async def test_setup_and_stop(hass, mock_bleak_scanner_start): """Test we and setup and stop the scanner.""" mock_bt = [ diff --git a/tests/conftest.py b/tests/conftest.py index 4b2852e94fc..ee2e903e88d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -167,7 +167,8 @@ def verify_cleanup(): pytest.exit(f"Detected non stopped instances ({count}), aborting test run") threads = frozenset(threading.enumerate()) - threads_before - assert not threads + for thread in threads: + assert isinstance(thread, threading._DummyThread) @pytest.fixture(autouse=True) @@ -855,3 +856,36 @@ def mock_integration_frame(): ], ): yield correct_frame + + +@pytest.fixture(name="mock_bleak_scanner_start") +def mock_bleak_scanner_start(): + """Fixture to mock starting the bleak scanner.""" + + # Late imports to avoid loading bleak unless we need it + + import bleak # pylint: disable=import-outside-toplevel + + from homeassistant.components.bluetooth import ( # pylint: disable=import-outside-toplevel + models as bluetooth_models, + ) + + scanner = bleak.BleakScanner + bluetooth_models.HA_BLEAK_SCANNER = None + + with patch("homeassistant.components.bluetooth.HaBleakScanner.stop"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ) as mock_bleak_scanner_start: + yield mock_bleak_scanner_start + + # We need to drop the stop method from the object since we patched + # out start and this fixture will expire before the stop method is called + # when EVENT_HOMEASSISTANT_STOP is fired. + if bluetooth_models.HA_BLEAK_SCANNER: + bluetooth_models.HA_BLEAK_SCANNER.stop = AsyncMock() + bleak.BleakScanner = scanner + + +@pytest.fixture(name="mock_bluetooth") +def mock_bluetooth(mock_bleak_scanner_start): + """Mock out bluetooth from starting.""" From a31dde9cb427104f1b22fb12d3013baf20a1dc76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 14:44:27 +0200 Subject: [PATCH 436/820] Await startup in homekit controller (#75021) --- .../components/homekit_controller/__init__.py | 6 ++ .../components/homekit_controller/camera.py | 2 +- .../homekit_controller/config_flow.py | 9 +- .../homekit_controller/connection.py | 53 +++++++----- tests/components/homekit_controller/common.py | 7 ++ .../homekit_controller/test_config_flow.py | 2 +- .../homekit_controller/test_init.py | 84 ++++++++++++++++++- 7 files changed, 133 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 67da7a1068f..bc911b991ca 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -227,6 +227,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await conn.async_setup(): del hass.data[KNOWN_DEVICES][conn.unique_id] + if (connection := getattr(conn.pairing, "connection")) and hasattr( + connection, "host" + ): + raise ConfigEntryNotReady( + f"Cannot connect to {connection.host}:{connection.port}" + ) raise ConfigEntryNotReady return True diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index 0ffa0a22f4d..0f0dd4f9050 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -25,7 +25,7 @@ class HomeKitCamera(AccessoryEntity, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a jpeg with the current camera snapshot.""" - return await self._accessory.pairing.image( + return await self._accessory.pairing.image( # type: ignore[attr-defined] self._aid, width or 640, height or 480, diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 493dd05a8b2..ac840eb0689 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr +from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES from .utils import async_get_controller @@ -240,17 +241,19 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( existing_entry, data={**existing_entry.data, **updated_ip_port} ) - conn = self.hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = self.hass.data[KNOWN_DEVICES][hkid] # When we rediscover the device, let aiohomekit know # that the device is available and we should not wait # to retry connecting any longer. reconnect_soon # will do nothing if the device is already connected await conn.pairing.reconnect_soon() - if conn.config_num != config_num: + if config_num and conn.config_num != config_num: _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid ) - self.hass.async_create_task(conn.async_refresh_entity_map(config_num)) + self.hass.async_create_task( + conn.async_refresh_entity_map_and_entities(config_num) + ) return self.async_abort(reason="already_configured") _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 7a85e234807..35f4c6bdb31 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -7,6 +7,7 @@ import datetime import logging from typing import Any +from aiohomekit import Controller from aiohomekit.exceptions import ( AccessoryDisconnectedError, AccessoryNotFoundError, @@ -35,6 +36,7 @@ from .const import ( IDENTIFIER_SERIAL_NUMBER, ) from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry +from .storage import EntityMapStorage DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds @@ -70,11 +72,13 @@ class HKDevice: # don't want to mutate a dict owned by a config entry. self.pairing_data = pairing_data.copy() - self.pairing = hass.data[CONTROLLER].load_pairing( + connection: Controller = hass.data[CONTROLLER] + + self.pairing = connection.load_pairing( self.pairing_data["AccessoryPairingID"], self.pairing_data ) - self.accessories = None + self.accessories: list[Any] | None = None self.config_num = 0 self.entity_map = Accessories() @@ -167,26 +171,23 @@ class HKDevice: async def async_setup(self) -> bool: """Prepare to use a paired HomeKit device in Home Assistant.""" - cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id) - if not cache: - if await self.async_refresh_entity_map(self.config_num): - self._polling_interval_remover = async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL - ) - return True + entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] + if cache := entity_storage.get_map(self.unique_id): + self.accessories = cache["accessories"] + self.config_num = cache["config_num"] + self.entity_map = Accessories.from_list(self.accessories) + elif not await self.async_refresh_entity_map(self.config_num): return False - self.accessories = cache["accessories"] - self.config_num = cache["config_num"] - - self.entity_map = Accessories.from_list(self.accessories) - + await self.async_process_entity_map() + if not self.pairing.is_connected: + return False + # If everything is up to date, we can create the entities + # since we know the data is not stale. + self.add_entities() self._polling_interval_remover = async_track_time_interval( self.hass, self.async_update, DEFAULT_SCAN_INTERVAL ) - - self.hass.async_create_task(self.async_process_entity_map()) - return True def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo: @@ -361,7 +362,7 @@ class HKDevice: # is especially important for BLE, as the Pairing instance relies on the entity map # to map aid/iid to GATT characteristics. So push it to there as well. - self.pairing.pairing_data["accessories"] = self.accessories + self.pairing.pairing_data["accessories"] = self.accessories # type: ignore[attr-defined] self.async_detect_workarounds() @@ -375,8 +376,6 @@ class HKDevice: # Load any triggers for this config entry await async_setup_triggers_for_entry(self.hass, self.config_entry) - self.add_entities() - if self.watchable_characteristics: await self.pairing.subscribe(self.watchable_characteristics) if not self.pairing.is_connected: @@ -395,6 +394,12 @@ class HKDevice: self.config_entry, self.platforms ) + async def async_refresh_entity_map_and_entities(self, config_num: int) -> None: + """Refresh the entity map and entities for this pairing.""" + await self.async_refresh_entity_map(config_num) + await self.async_process_entity_map() + self.add_entities() + async def async_refresh_entity_map(self, config_num: int) -> bool: """Handle setup of a HomeKit accessory.""" try: @@ -404,15 +409,17 @@ class HKDevice: # later when Bonjour spots c# is still not up to date. return False + assert self.accessories is not None + self.entity_map = Accessories.from_list(self.accessories) - self.hass.data[ENTITY_MAP].async_create_or_update_map( + entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] + + entity_storage.async_create_or_update_map( self.unique_id, config_num, self.accessories ) self.config_num = config_num - self.hass.async_create_task(self.async_process_entity_map()) - return True def add_accessory_factory(self, add_entities_cb) -> None: diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 4cad585d135..e773b2ffc66 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -186,6 +186,13 @@ async def setup_platform(hass): async def setup_test_accessories(hass, accessories): """Load a fake homekit device based on captured JSON profile.""" fake_controller = await setup_platform(hass) + return await setup_test_accessories_with_controller( + hass, accessories, fake_controller + ) + + +async def setup_test_accessories_with_controller(hass, accessories, fake_controller): + """Load a fake homekit device based on captured JSON profile.""" pairing_id = "00:00:00:00:00:00" diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 7b62c1e9d6d..9535b7d0cd5 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -515,7 +515,7 @@ async def test_discovery_already_configured_update_csharp(hass, controller): assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryPort"] == discovery_info.port - assert connection_mock.async_refresh_entity_map.await_count == 1 + assert connection_mock.async_refresh_entity_map_and_entities.await_count == 1 @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 84a6c8b86bf..5b4d5c74ff6 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -1,19 +1,26 @@ """Tests for homekit_controller init.""" +from datetime import timedelta from unittest.mock import patch +from aiohomekit import exceptions +from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from aiohomekit.testing import FakeController, FakeDiscovery, FakePairing -from homeassistant.components.homekit_controller.const import ENTITY_MAP +from homeassistant.components.homekit_controller.const import DOMAIN, ENTITY_MAP +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -from .common import Helper, remove_device +from .common import Helper, remove_device, setup_test_accessories_with_controller +from tests.common import async_fire_time_changed from tests.components.homekit_controller.common import setup_test_component ALIVE_DEVICE_NAME = "testdevice" @@ -89,3 +96,76 @@ async def test_device_remove_devices(hass, hass_ws_client): await remove_device(await hass_ws_client(hass), dead_device_entry.id, entry_id) is True ) + + +async def test_offline_device_raises(hass): + """Test an offline device raises ConfigEntryNotReady.""" + + is_connected = False + + class OfflineFakePairing(FakePairing): + """Fake pairing that always returns False for is_connected.""" + + @property + def is_connected(self): + nonlocal is_connected + return is_connected + + class OfflineFakeDiscovery(FakeDiscovery): + """Fake discovery that returns an offline pairing.""" + + async def start_pairing(self, alias: str): + if self.description.id in self.controller.pairings: + raise exceptions.AlreadyPairedError( + f"{self.description.id} already paired" + ) + + async def finish_pairing(pairing_code): + if pairing_code != self.pairing_code: + raise exceptions.AuthenticationError("M4") + pairing_data = {} + pairing_data["AccessoryIP"] = self.info["address"] + pairing_data["AccessoryPort"] = self.info["port"] + pairing_data["Connection"] = "IP" + + obj = self.controller.pairings[alias] = OfflineFakePairing( + self.controller, pairing_data, self.accessories + ) + return obj + + return finish_pairing + + class OfflineFakeController(FakeController): + """Fake controller that always returns a discovery with a pairing that always returns False for is_connected.""" + + def add_device(self, accessories): + device_id = "00:00:00:00:00:00" + discovery = self.discoveries[device_id] = OfflineFakeDiscovery( + self, + device_id, + accessories=accessories, + ) + return discovery + + with patch( + "homeassistant.components.homekit_controller.utils.Controller" + ) as controller: + fake_controller = controller.return_value = OfflineFakeController() + await async_setup_component(hass, DOMAIN, {}) + + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) + + config_entry, _ = await setup_test_accessories_with_controller( + hass, [accessory], fake_controller + ) + + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + is_connected = True + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED From 2286dea636fda001f03433ba14d7adbda43979e5 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 14 Jul 2022 10:55:24 -0400 Subject: [PATCH 437/820] Bump version of pyunifiprotect to 4.0.10 (#75180) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 9aeb8b48050..35dafc96927 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.9", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index f93afa0debd..2904f51d6c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.9 +pyunifiprotect==4.0.10 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1959df8456..8a3f0532550 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.9 +pyunifiprotect==4.0.10 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 89985b93fbf9335ba0380db23126b3f20b220a65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 17:17:16 +0200 Subject: [PATCH 438/820] Avoid loading platforms in HKC if we are going to raise ConfigEntryNotReady (#75177) --- .../homekit_controller/connection.py | 39 ++++++++++++++++--- .../homekit_controller/test_init.py | 5 ++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 35f4c6bdb31..e1cbedb01e8 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -14,7 +14,7 @@ from aiohomekit.exceptions import ( EncryptionError, ) from aiohomekit.model import Accessories, Accessory -from aiohomekit.model.characteristics import Characteristic +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.services import Service from homeassistant.const import ATTR_VIA_DEVICE @@ -169,6 +169,29 @@ class HKDevice: self.available = available async_dispatcher_send(self.hass, self.signal_state_updated) + async def async_ensure_available(self) -> bool: + """Verify the accessory is available after processing the entity map.""" + if self.available: + return True + if self.watchable_characteristics and self.pollable_characteristics: + # We already tried, no need to try again + return False + # We there are no watchable and not pollable characteristics, + # we need to force a connection to the device to verify its alive. + # + # This is similar to iOS's behavior for keeping alive connections + # to cameras. + # + primary = self.entity_map.accessories[0] + aid = primary.aid + iid = primary.accessory_information[CharacteristicsTypes.SERIAL_NUMBER].iid + try: + await self.pairing.get_characteristics([(aid, iid)]) + except (AccessoryDisconnectedError, EncryptionError, AccessoryNotFoundError): + return False + self.async_set_available_state(True) + return True + async def async_setup(self) -> bool: """Prepare to use a paired HomeKit device in Home Assistant.""" entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] @@ -180,16 +203,22 @@ class HKDevice: return False await self.async_process_entity_map() - if not self.pairing.is_connected: + + if not await self.async_ensure_available(): return False # If everything is up to date, we can create the entities # since we know the data is not stale. - self.add_entities() + await self.async_add_new_entities() self._polling_interval_remover = async_track_time_interval( self.hass, self.async_update, DEFAULT_SCAN_INTERVAL ) return True + async def async_add_new_entities(self) -> None: + """Add new entities to Home Assistant.""" + await self.async_load_platforms() + self.add_entities() + def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo: """Build a DeviceInfo for a given accessory.""" identifiers = { @@ -369,8 +398,6 @@ class HKDevice: # Migrate to new device ids self.async_migrate_devices() - await self.async_load_platforms() - self.async_create_devices() # Load any triggers for this config entry @@ -398,7 +425,7 @@ class HKDevice: """Refresh the entity map and entities for this pairing.""" await self.async_refresh_entity_map(config_num) await self.async_process_entity_map() - self.add_entities() + await self.async_add_new_entities() async def async_refresh_entity_map(self, config_num: int) -> bool: """Handle setup of a HomeKit accessory.""" diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 5b4d5c74ff6..841b726ac0e 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import patch -from aiohomekit import exceptions +from aiohomekit import AccessoryDisconnectedError, exceptions from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -111,6 +111,9 @@ async def test_offline_device_raises(hass): nonlocal is_connected return is_connected + def get_characteristics(self, chars, *args, **kwargs): + raise AccessoryDisconnectedError("any") + class OfflineFakeDiscovery(FakeDiscovery): """Fake discovery that returns an offline pairing.""" From 1725948d4a527f71eb6681dd03899a7d128c4b50 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 18:06:33 +0200 Subject: [PATCH 439/820] Use instance attributes in minecraft_server (#75157) * Remove minecraft_server from mypy ignore list * Use new entity naming style --- .../components/minecraft_server/__init__.py | 62 +++++++------------ .../minecraft_server/binary_sensor.py | 9 +-- .../components/minecraft_server/helpers.py | 4 +- .../components/minecraft_server/sensor.py | 44 ++++--------- mypy.ini | 9 --- script/hassfest/mypy_config.py | 3 - 6 files changed, 42 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 1b4a71e8ab8..4abfbca9a2f 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -1,21 +1,22 @@ """The Minecraft Server integration.""" from __future__ import annotations +from collections.abc import Mapping from datetime import datetime, timedelta import logging +from typing import Any from mcstatus.server import MinecraftServer as MCStatus from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType from . import helpers from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX @@ -30,6 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: domain_data = hass.data.setdefault(DOMAIN, {}) # Create and store server instance. + assert entry.unique_id unique_id = entry.unique_id _LOGGER.debug( "Creating server instance for '%s' (%s)", @@ -71,7 +73,7 @@ class MinecraftServer: _MAX_RETRIES_STATUS = 3 def __init__( - self, hass: HomeAssistant, unique_id: str, config_data: ConfigType + self, hass: HomeAssistant, unique_id: str, config_data: Mapping[str, Any] ) -> None: """Initialize server instance.""" self._hass = hass @@ -94,14 +96,14 @@ class MinecraftServer: self.latency_time = None self.players_online = None self.players_max = None - self.players_list = None + self.players_list: list[str] | None = None self.motd = None # Dispatcher signal name self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" # Callback for stopping periodic update. - self._stop_periodic_update = None + self._stop_periodic_update: CALLBACK_TYPE | None = None def start_periodic_update(self) -> None: """Start periodic execution of update method.""" @@ -111,7 +113,8 @@ class MinecraftServer: def stop_periodic_update(self) -> None: """Stop periodic execution of update method.""" - self._stop_periodic_update() + if self._stop_periodic_update: + self._stop_periodic_update() async def async_check_connection(self) -> None: """Check server connection using a 'status' request and store connection status.""" @@ -219,14 +222,21 @@ class MinecraftServer: class MinecraftServerEntity(Entity): """Representation of a Minecraft Server base entity.""" + _attr_has_entity_name = True + _attr_should_poll = False + def __init__( - self, server: MinecraftServer, type_name: str, icon: str, device_class: str + self, + server: MinecraftServer, + type_name: str, + icon: str, + device_class: str | None, ) -> None: """Initialize base entity.""" self._server = server - self._name = f"{server.name} {type_name}" - self._icon = icon - self._unique_id = f"{self._server.unique_id}-{type_name}" + self._attr_name = type_name + self._attr_icon = icon + self._attr_unique_id = f"{self._server.unique_id}-{type_name}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._server.unique_id)}, manufacturer=MANUFACTURER, @@ -234,34 +244,9 @@ class MinecraftServerEntity(Entity): name=self._server.name, sw_version=self._server.protocol_version, ) - self._device_class = device_class + self._attr_device_class = device_class self._extra_state_attributes = None - self._disconnect_dispatcher = None - - @property - def name(self) -> str: - """Return name.""" - return self._name - - @property - def unique_id(self) -> str: - """Return unique ID.""" - return self._unique_id - - @property - def device_class(self) -> str: - """Return device class.""" - return self._device_class - - @property - def icon(self) -> str: - """Return icon.""" - return self._icon - - @property - def should_poll(self) -> bool: - """Disable polling.""" - return False + self._disconnect_dispatcher: CALLBACK_TYPE | None = None async def async_update(self) -> None: """Fetch data from the server.""" @@ -275,7 +260,8 @@ class MinecraftServerEntity(Entity): async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher before removal.""" - self._disconnect_dispatcher() + if self._disconnect_dispatcher: + self._disconnect_dispatcher() @callback def _update_callback(self) -> None: diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index 75c572f366f..0bf4cdab859 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -37,13 +37,8 @@ class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorEntit icon=ICON_STATUS, device_class=BinarySensorDeviceClass.CONNECTIVITY, ) - self._is_on = False - - @property - def is_on(self) -> bool: - """Return binary state.""" - return self._is_on + self._attr_is_on = False async def async_update(self) -> None: """Update status.""" - self._is_on = self._server.online + self._attr_is_on = self._server.online diff --git a/homeassistant/components/minecraft_server/helpers.py b/homeassistant/components/minecraft_server/helpers.py index 13ec4cd1afb..7153c170a6a 100644 --- a/homeassistant/components/minecraft_server/helpers.py +++ b/homeassistant/components/minecraft_server/helpers.py @@ -11,7 +11,9 @@ from homeassistant.core import HomeAssistant from .const import SRV_RECORD_PREFIX -async def async_check_srv_record(hass: HomeAssistant, host: str) -> dict[str, Any]: +async def async_check_srv_record( + hass: HomeAssistant, host: str +) -> dict[str, Any] | None: """Check if the given host is a valid Minecraft SRV record.""" # Check if 'host' is a valid SRV record. return_value = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index d7ca73d1411..f10a359eca0 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -1,8 +1,6 @@ """The Minecraft Server sensor platform.""" from __future__ import annotations -from typing import Any - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MILLISECONDS @@ -62,30 +60,19 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): self, server: MinecraftServer, type_name: str, - icon: str = None, - unit: str = None, - device_class: str = None, + icon: str, + unit: str | None, + device_class: str | None = None, ) -> None: """Initialize sensor base entity.""" super().__init__(server, type_name, icon, device_class) - self._state = None - self._unit = unit + self._attr_native_unit_of_measurement = unit @property def available(self) -> bool: """Return sensor availability.""" return self._server.online - @property - def native_value(self) -> Any: - """Return sensor state.""" - return self._state - - @property - def native_unit_of_measurement(self) -> str: - """Return sensor measurement unit.""" - return self._unit - class MinecraftServerVersionSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server version sensor.""" @@ -98,7 +85,7 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update version.""" - self._state = self._server.version + self._attr_native_value = self._server.version class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): @@ -115,7 +102,7 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update protocol version.""" - self._state = self._server.protocol_version + self._attr_native_value = self._server.protocol_version class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): @@ -132,7 +119,7 @@ class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update latency time.""" - self._state = self._server.latency_time + self._attr_native_value = self._server.latency_time class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): @@ -149,20 +136,15 @@ class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update online players state and device state attributes.""" - self._state = self._server.players_online + self._attr_native_value = self._server.players_online - extra_state_attributes = None + extra_state_attributes = {} players_list = self._server.players_list if players_list is not None and len(players_list) != 0: - extra_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list} + extra_state_attributes[ATTR_PLAYERS_LIST] = self._server.players_list - self._extra_state_attributes = extra_state_attributes - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return players list in device state attributes.""" - return self._extra_state_attributes + self._attr_extra_state_attributes = extra_state_attributes class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): @@ -179,7 +161,7 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update maximum number of players.""" - self._state = self._server.players_max + self._attr_native_value = self._server.players_max class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): @@ -196,4 +178,4 @@ class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update MOTD.""" - self._state = self._server.motd + self._attr_native_value = self._server.motd diff --git a/mypy.ini b/mypy.ini index 2297869dbb0..b936eab1fd0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2665,15 +2665,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome] ignore_errors = true -[mypy-homeassistant.components.minecraft_server] -ignore_errors = true - -[mypy-homeassistant.components.minecraft_server.helpers] -ignore_errors = true - -[mypy-homeassistant.components.minecraft_server.sensor] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f53d6a6e6b0..c7bdd8e6c2b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -19,9 +19,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", "homeassistant.components.evohome", - "homeassistant.components.minecraft_server", - "homeassistant.components.minecraft_server.helpers", - "homeassistant.components.minecraft_server.sensor", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 666f715e76faf1061aed7cb4ec59611b7b2f4455 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 18:09:09 +0200 Subject: [PATCH 440/820] Avoid importing MQTT into core for ServiceInfo dataclass (#74418) * Avoid importing MQTT into core for discovery dataclass Likely fixes #73863 * relo * adjust * rename * rename * rename * adjust missed imports * drop compat * fix conflict correctly * Update homeassistant/helpers/config_entry_flow.py * fix black from trying to fix the conflict in github --- homeassistant/components/mqtt/__init__.py | 15 -------------- homeassistant/components/mqtt/discovery.py | 3 ++- homeassistant/components/mqtt/models.py | 2 +- .../components/tasmota/config_flow.py | 3 ++- homeassistant/config_entries.py | 2 +- homeassistant/helpers/config_entry_flow.py | 20 +++++++++++-------- homeassistant/helpers/service_info/mqtt.py | 20 +++++++++++++++++++ tests/components/tasmota/test_config_flow.py | 10 +++++----- 8 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 homeassistant/helpers/service_info/mqtt.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 906923138b9..394e4af3e2e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -3,8 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Callable -from dataclasses import dataclass -import datetime as dt import logging from typing import Any, cast @@ -23,7 +21,6 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback -from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry @@ -145,18 +142,6 @@ MQTT_PUBLISH_SCHEMA = vol.All( ) -@dataclass -class MqttServiceInfo(BaseServiceInfo): - """Prepared info from mqtt entries.""" - - topic: str - payload: ReceivePayloadType - qos: int - retain: bool - subscribed_topic: str - timestamp: dt.datetime - - async def _async_setup_discovery( hass: HomeAssistant, conf: ConfigType, config_entry ) -> None: diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index a480cdd5680..ebc3a170aeb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -17,6 +17,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.json import json_loads +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -267,7 +268,7 @@ async def async_start( # noqa: C901 if key not in hass.data[INTEGRATION_UNSUBSCRIBE]: return - data = mqtt.MqttServiceInfo( + data = MqttServiceInfo( topic=msg.topic, payload=msg.payload, qos=msg.qos, diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 9bce6baab8b..d5560f6954e 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -12,12 +12,12 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import template from homeassistant.helpers.entity import Entity +from homeassistant.helpers.service_info.mqtt import ReceivePayloadType from homeassistant.helpers.typing import TemplateVarsType _SENTINEL = object() PublishPayloadType = Union[str, bytes, int, float, None] -ReceivePayloadType = Union[str, bytes] @attr.s(slots=True, frozen=True) diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index e2109a16afe..d8981090d58 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -6,8 +6,9 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.mqtt import MqttServiceInfo, valid_subscribe_topic +from homeassistant.components.mqtt import valid_subscribe_topic from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 4b76f63681a..6e2b70be7cc 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -31,10 +31,10 @@ if TYPE_CHECKING: from .components.bluetooth import BluetoothServiceInfo from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo - from .components.mqtt import MqttServiceInfo from .components.ssdp import SsdpServiceInfo from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo + from .helpers.service_info.mqtt import MqttServiceInfo _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 75a4dcd20f4..6ee04ea6d9b 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import bluetooth, dhcp, onboarding, ssdp, zeroconf +from homeassistant.components import onboarding from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -15,8 +15,12 @@ from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType if TYPE_CHECKING: import asyncio - from homeassistant.components import mqtt + from homeassistant.components.bluetooth import BluetoothServiceInfo + from homeassistant.components.dhcp import DhcpServiceInfo + from homeassistant.components.ssdp import SsdpServiceInfo + from homeassistant.components.zeroconf import ZeroconfServiceInfo + from .service_info.mqtt import MqttServiceInfo _R = TypeVar("_R", bound="Awaitable[bool] | bool") DiscoveryFunctionType = Callable[[HomeAssistant], _R] @@ -93,7 +97,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_bluetooth( - self, discovery_info: bluetooth.BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfo ) -> FlowResult: """Handle a flow initialized by bluetooth discovery.""" if self._async_in_progress() or self._async_current_entries(): @@ -103,7 +107,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: """Handle a flow initialized by dhcp discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -113,7 +117,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_homekit( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by Homekit discovery.""" if self._async_in_progress() or self._async_current_entries(): @@ -123,7 +127,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_mqtt(self, discovery_info: mqtt.MqttServiceInfo) -> FlowResult: + async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: """Handle a flow initialized by mqtt discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -133,7 +137,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_zeroconf( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by Zeroconf discovery.""" if self._async_in_progress() or self._async_current_entries(): @@ -143,7 +147,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp(self, discovery_info: SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by Ssdp discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/helpers/service_info/mqtt.py b/homeassistant/helpers/service_info/mqtt.py new file mode 100644 index 00000000000..fcf5d4744f1 --- /dev/null +++ b/homeassistant/helpers/service_info/mqtt.py @@ -0,0 +1,20 @@ +"""MQTT Discovery data.""" +from dataclasses import dataclass +import datetime as dt +from typing import Union + +from homeassistant.data_entry_flow import BaseServiceInfo + +ReceivePayloadType = Union[str, bytes] + + +@dataclass +class MqttServiceInfo(BaseServiceInfo): + """Prepared info from mqtt entries.""" + + topic: str + payload: ReceivePayloadType + qos: int + retain: bool + subscribed_topic: str + timestamp: dt.datetime diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 3413817892b..77a2787c0d5 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -1,6 +1,6 @@ """Test config flow.""" from homeassistant import config_entries -from homeassistant.components import mqtt +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_mqtt_abort_if_existing_entry(hass, mqtt_mock): async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): """Check MQTT flow aborts if discovery topic is invalid.""" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/bla", payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' @@ -42,7 +42,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): assert result["type"] == "abort" assert result["reason"] == "invalid_discovery_info" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/config", payload="", qos=0, @@ -56,7 +56,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): assert result["type"] == "abort" assert result["reason"] == "invalid_discovery_info" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/config", payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' @@ -81,7 +81,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): async def test_mqtt_setup(hass, mqtt_mock) -> None: """Test we can finish a config flow through MQTT with custom prefix.""" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/config", payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' From bb14f83b94fbe8e1eea486094f8320117dc4bcf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 18:30:20 +0200 Subject: [PATCH 441/820] Bump unifi-discovery to 1.1.5 (#75189) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/pip_check | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 35dafc96927..2410ea91399 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 2904f51d6c3..04ca43b481d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2358,7 +2358,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.4 +unifi-discovery==1.1.5 # homeassistant.components.unifiled unifiled==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a3f0532550..e91630a0610 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1570,7 +1570,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.4 +unifi-discovery==1.1.5 # homeassistant.components.upb upb_lib==0.4.12 diff --git a/script/pip_check b/script/pip_check index 6f48ce15bb1..f29ea5a5dd0 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=7 +DEPENDENCY_CONFLICTS=6 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From 56da7d0ad06fdeef8e409ba85d114c84b883a8f8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 19:00:06 +0200 Subject: [PATCH 442/820] Allow Mjpeg camera name to be None (#75002) --- homeassistant/components/mjpeg/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index 91b2271aafa..5c42d392142 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -85,7 +85,7 @@ class MjpegCamera(Camera): def __init__( self, *, - name: str, + name: str | None = None, mjpeg_url: str, still_image_url: str | None, authentication: str | None = None, From 5e46fa6f8ba4c2f8f21421bfefe9fb422b713437 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:38:22 -0400 Subject: [PATCH 443/820] Skip `iso4217` version 1.10, which includes a broken `__init__.pyi` file (#75200) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 72d88ab7cf5..09eb66c5f34 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -118,3 +118,7 @@ pydantic!=1.9.1 # Breaks asyncio # https://github.com/pubnub/python/issues/130 pubnub!=6.4.0 + +# Package's __init__.pyi stub has invalid syntax and breaks mypy +# https://github.com/dahlia/iso4217/issues/16 +iso4217!=1.10.20220401 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7bdecd9e0c2..dcc53a73df0 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -136,6 +136,10 @@ pydantic!=1.9.1 # Breaks asyncio # https://github.com/pubnub/python/issues/130 pubnub!=6.4.0 + +# Package's __init__.pyi stub has invalid syntax and breaks mypy +# https://github.com/dahlia/iso4217/issues/16 +iso4217!=1.10.20220401 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From fde3489e86bc22eb00a21e59e3ef13406b9a573f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 20:36:54 +0200 Subject: [PATCH 444/820] Relocate BluetoothServiceInfo to helpers.service_info (#75195) --- .../components/bluetooth/__init__.py | 48 +-------------- .../bluetooth_tracker/device_tracker.py | 6 +- homeassistant/config_entries.py | 2 +- homeassistant/helpers/config_entry_flow.py | 2 +- .../helpers/service_info/bluetooth.py | 58 +++++++++++++++++++ 5 files changed, 65 insertions(+), 51 deletions(-) create mode 100644 homeassistant/helpers/service_info/bluetooth.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 26c1af3fb92..01f55048c6d 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -2,16 +2,14 @@ from __future__ import annotations from collections.abc import Callable -import dataclasses from enum import Enum import fnmatch -from functools import cached_property import logging import platform from typing import Final, TypedDict from bleak import BleakError -from bleak.backends.device import MANUFACTURERS, BLEDevice +from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from lru import LRU # pylint: disable=no-name-in-module @@ -23,8 +21,8 @@ from homeassistant.core import ( HomeAssistant, callback as hass_callback, ) -from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.loader import ( BluetoothMatcher, @@ -74,48 +72,6 @@ MANUFACTURER_ID: Final = "manufacturer_id" MANUFACTURER_DATA_FIRST_BYTE: Final = "manufacturer_data_first_byte" -@dataclasses.dataclass -class BluetoothServiceInfo(BaseServiceInfo): - """Prepared info from bluetooth entries.""" - - name: str - address: str - rssi: int - manufacturer_data: dict[int, bytes] - service_data: dict[str, bytes] - service_uuids: list[str] - - @classmethod - def from_advertisement( - cls, device: BLEDevice, advertisement_data: AdvertisementData - ) -> BluetoothServiceInfo: - """Create a BluetoothServiceInfo from an advertisement.""" - return cls( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=device.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - ) - - @cached_property - def manufacturer(self) -> str | None: - """Convert manufacturer data to a string.""" - for manufacturer in self.manufacturer_data: - if manufacturer in MANUFACTURERS: - name: str = MANUFACTURERS[manufacturer] - return name - return None - - @cached_property - def manufacturer_id(self) -> int | None: - """Get the first manufacturer id.""" - for manufacturer in self.manufacturer_data: - return manufacturer - return None - - BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[[BluetoothServiceInfo, BluetoothChange], None] diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index b62333c0489..5cf173d356e 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -61,7 +61,7 @@ def is_bluetooth_device(device: Device) -> bool: def discover_devices(device_id: int) -> list[tuple[str, str]]: """Discover Bluetooth devices.""" try: - result = bluetooth.discover_devices( + result = bluetooth.discover_devices( # type: ignore[attr-defined] duration=8, lookup_names=True, flush_cache=True, @@ -124,7 +124,7 @@ async def get_tracking_devices(hass: HomeAssistant) -> tuple[set[str], set[str]] def lookup_name(mac: str) -> str | None: """Lookup a Bluetooth device name.""" _LOGGER.debug("Scanning %s", mac) - return bluetooth.lookup_name(mac, timeout=5) # type: ignore[no-any-return] + return bluetooth.lookup_name(mac, timeout=5) # type: ignore[attr-defined,no-any-return] async def async_setup_scanner( @@ -180,7 +180,7 @@ async def async_setup_scanner( if tasks: await asyncio.wait(tasks) - except bluetooth.BluetoothError: + except bluetooth.BluetoothError: # type: ignore[attr-defined] _LOGGER.exception("Error looking up Bluetooth device") async def update_bluetooth(now: datetime | None = None) -> None: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6e2b70be7cc..e5a36b980ed 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -28,12 +28,12 @@ from .util import uuid as uuid_util from .util.decorator import Registry if TYPE_CHECKING: - from .components.bluetooth import BluetoothServiceInfo from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo from .components.ssdp import SsdpServiceInfo from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo + from .helpers.service_info.bluetooth import BluetoothServiceInfo from .helpers.service_info.mqtt import MqttServiceInfo _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6ee04ea6d9b..847a90a0d1b 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -15,11 +15,11 @@ from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType if TYPE_CHECKING: import asyncio - from homeassistant.components.bluetooth import BluetoothServiceInfo from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo + from .service_info.bluetooth import BluetoothServiceInfo from .service_info.mqtt import MqttServiceInfo _R = TypeVar("_R", bound="Awaitable[bool] | bool") diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py new file mode 100644 index 00000000000..9dff2782da0 --- /dev/null +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -0,0 +1,58 @@ +"""The bluetooth integration service info.""" +from __future__ import annotations + +import dataclasses +from functools import cached_property +from typing import TYPE_CHECKING + +from homeassistant.data_entry_flow import BaseServiceInfo + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + +@dataclasses.dataclass +class BluetoothServiceInfo(BaseServiceInfo): + """Prepared info from bluetooth entries.""" + + name: str + address: str + rssi: int + manufacturer_data: dict[int, bytes] + service_data: dict[str, bytes] + service_uuids: list[str] + + @classmethod + def from_advertisement( + cls, device: BLEDevice, advertisement_data: AdvertisementData + ) -> BluetoothServiceInfo: + """Create a BluetoothServiceInfo from an advertisement.""" + return cls( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + ) + + @cached_property + def manufacturer(self) -> str | None: + """Convert manufacturer data to a string.""" + from bleak.backends.device import ( # pylint: disable=import-outside-toplevel + MANUFACTURERS, + ) + + for manufacturer in self.manufacturer_data: + if manufacturer in MANUFACTURERS: + name: str = MANUFACTURERS[manufacturer] + return name + return None + + @cached_property + def manufacturer_id(self) -> int | None: + """Get the first manufacturer id.""" + for manufacturer in self.manufacturer_data: + return manufacturer + return None From 5f08052f40b1d1eca574ec546b5d8acb33a148fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 20:48:30 +0200 Subject: [PATCH 445/820] Move lutron_caseta migration to happen after successful setup (#75204) --- homeassistant/components/lutron_caseta/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index cea069a1556..d235f294b3a 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -143,8 +143,6 @@ async def async_setup_entry( ca_certs = hass.config.path(config_entry.data[CONF_CA_CERTS]) bridge = None - await _async_migrate_unique_ids(hass, config_entry) - try: bridge = Smartbridge.create_tls( hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs @@ -167,6 +165,7 @@ async def async_setup_entry( raise ConfigEntryNotReady(f"Cannot connect to {host}") _LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host) + await _async_migrate_unique_ids(hass, config_entry) devices = bridge.get_devices() bridge_device = devices[BRIDGE_DEVICE_ID] From 124bfe1629618cb3d6f0ae9d1db054431e4e349a Mon Sep 17 00:00:00 2001 From: Khole Date: Thu, 14 Jul 2022 19:50:35 +0100 Subject: [PATCH 446/820] Fix Hive power unit of measurement (#75210) --- homeassistant/components/hive/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index d224ec085f3..809feb19d5e 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, POWER_KILO_WATT +from homeassistant.const import PERCENTAGE, POWER_WATT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -28,7 +28,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="Power", - native_unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, From 874043f5962f03c5f54b4f27462fbb7a7dc0126b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 14 Jul 2022 21:01:57 +0200 Subject: [PATCH 447/820] Migrate Axis to new entity naming style (#74735) --- homeassistant/components/axis/axis_base.py | 4 +++- homeassistant/components/axis/binary_sensor.py | 6 ++---- homeassistant/components/axis/camera.py | 1 - homeassistant/components/axis/light.py | 2 +- homeassistant/components/axis/switch.py | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 791764dc605..3d887478528 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -10,6 +10,8 @@ from .const import DOMAIN as AXIS_DOMAIN class AxisEntityBase(Entity): """Base common to all Axis entities.""" + _attr_has_entity_name = True + def __init__(self, device): """Initialize the Axis event.""" self.device = device @@ -47,7 +49,7 @@ class AxisEventBase(AxisEntityBase): super().__init__(device) self.event = event - self._attr_name = f"{device.name} {event.TYPE} {event.id}" + self._attr_name = f"{event.TYPE} {event.id}" self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}" self._attr_device_class = event.CLASS diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index d2b6a9f7dd7..fba7e8d6248 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -110,9 +110,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): and self.event.id in self.device.api.vapix.ports and self.device.api.vapix.ports[self.event.id].name ): - return ( - f"{self.device.name} {self.device.api.vapix.ports[self.event.id].name}" - ) + return self.device.api.vapix.ports[self.event.id].name if self.event.CLASS == CLASS_MOTION: @@ -128,6 +126,6 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): and event_data and self.event.id in event_data ): - return f"{self.device.name} {self.event.TYPE} {event_data[self.event.id].name}" + return f"{self.event.TYPE} {event_data[self.event.id].name}" return self._attr_name diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index bd46ae54f81..4df9a5e2141 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -40,7 +40,6 @@ class AxisCamera(AxisEntityBase, MjpegCamera): MjpegCamera.__init__( self, - name=device.name, username=device.username, password=device.password, mjpeg_url=self.mjpeg_source, diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index 30f1cee9340..e34c0d4a2d6 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -53,7 +53,7 @@ class AxisLight(AxisEventBase, LightEntity): self.max_intensity = 0 light_type = device.api.vapix.light_control[self.light_id].light_type - self._attr_name = f"{device.name} {light_type} {event.TYPE} {event.id}" + self._attr_name = f"{light_type} {event.TYPE} {event.id}" self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_color_mode = ColorMode.BRIGHTNESS diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index a778c974737..61f16cfc789 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -40,7 +40,7 @@ class AxisSwitch(AxisEventBase, SwitchEntity): super().__init__(event, device) if event.id and device.api.vapix.ports[event.id].name: - self._attr_name = f"{device.name} {device.api.vapix.ports[event.id].name}" + self._attr_name = device.api.vapix.ports[event.id].name @property def is_on(self): From 3f6e9304898431b7073842e94690417a3f32dec6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 21:03:12 +0200 Subject: [PATCH 448/820] Bump nexia to 2.0.2 (#75209) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index cc5e6de8641..e381dc95897 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.1"], + "requirements": ["nexia==2.0.2"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 04ca43b481d..ad537da36ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1086,7 +1086,7 @@ nettigo-air-monitor==1.3.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.1 +nexia==2.0.2 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e91630a0610..a0bf0ab1d8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -757,7 +757,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.3.0 # homeassistant.components.nexia -nexia==2.0.1 +nexia==2.0.2 # homeassistant.components.discord nextcord==2.0.0a8 From 54a939e22332a45bfd60b551268348ad71c3f0d4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 14 Jul 2022 21:04:41 +0200 Subject: [PATCH 449/820] Migrate Filesize to new entity naming style (#75199) --- homeassistant/components/filesize/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 5f66aefaab5..06e567f14cb 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -119,6 +119,7 @@ class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): """Filesize sensor.""" entity_description: SensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -130,7 +131,7 @@ class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): """Initialize the Filesize sensor.""" super().__init__(coordinator) base_name = path.split("/")[-1] - self._attr_name = f"{base_name} {description.name}" + self._attr_name = description.name self._attr_unique_id = ( entry_id if description.key == "file" else f"{entry_id}-{description.key}" ) From 3d2101cac53a08644d6d4bf1e14b9f99fd874e7c Mon Sep 17 00:00:00 2001 From: Peter Galantha Date: Thu, 14 Jul 2022 12:14:25 -0700 Subject: [PATCH 450/820] Add total state_class for esphome (#75015) --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index a8a76c2b0c8..cf748b27170 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.10.0"], + "requirements": ["aioesphomeapi==10.11.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 45a73a5e5af..897ab86b18a 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -67,6 +67,7 @@ _STATE_CLASSES: EsphomeEnumMapper[ EsphomeSensorStateClass.NONE: None, EsphomeSensorStateClass.MEASUREMENT: SensorStateClass.MEASUREMENT, EsphomeSensorStateClass.TOTAL_INCREASING: SensorStateClass.TOTAL_INCREASING, + EsphomeSensorStateClass.TOTAL: SensorStateClass.TOTAL, } ) diff --git a/requirements_all.txt b/requirements_all.txt index ad537da36ff..24850836455 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -150,7 +150,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.10.0 +aioesphomeapi==10.11.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0bf0ab1d8a..22a93dacae8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.10.0 +aioesphomeapi==10.11.0 # homeassistant.components.flo aioflo==2021.11.0 From b7cdf5412b8b30b85c1668eda1d9f7b294fdd60c Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 14 Jul 2022 15:14:41 -0400 Subject: [PATCH 451/820] Bumped AIOAladdin Connect to 0.1.24 (#75182) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index bdf906ad200..8ef86a76bf8 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.23"], + "requirements": ["AIOAladdinConnect==0.1.24"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 24850836455..e0f109c4a14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.23 +AIOAladdinConnect==0.1.24 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22a93dacae8..6f1b457ade5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.23 +AIOAladdinConnect==0.1.24 # homeassistant.components.adax Adax-local==0.1.4 From 5287980f48279d4add33252b82cb3a63ff67d989 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 21:15:37 +0200 Subject: [PATCH 452/820] Remove template from mypy ignore list (#74426) --- homeassistant/components/number/__init__.py | 2 +- homeassistant/components/template/number.py | 8 ++++---- homeassistant/components/template/sensor.py | 2 +- mypy.ini | 5 ----- script/hassfest/mypy_config.py | 2 -- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 8c1800697a3..5ab1eaa7be4 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -200,7 +200,7 @@ class NumberEntity(Entity): _attr_native_max_value: float _attr_native_min_value: float _attr_native_step: float - _attr_native_value: float + _attr_native_value: float | None = None _attr_native_unit_of_measurement: str | None _deprecated_number_entity_reported = False _number_option_unit_of_measurement: str | None = None diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index d41bdee597b..89f4d22b957 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -14,6 +14,7 @@ from homeassistant.components.number.const import ( ATTR_VALUE, DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, + DEFAULT_STEP, DOMAIN as NUMBER_DOMAIN, ) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID @@ -119,10 +120,9 @@ class TemplateNumber(TemplateEntity, NumberEntity): self._min_value_template = config[ATTR_MIN] self._max_value_template = config[ATTR_MAX] self._attr_assumed_state = self._optimistic = config[CONF_OPTIMISTIC] - self._attr_native_value = None - self._attr_native_step = None - self._attr_native_min_value = None - self._attr_native_max_value = None + self._attr_native_step = DEFAULT_STEP + self._attr_native_min_value = DEFAULT_MIN_VALUE + self._attr_native_max_value = DEFAULT_MAX_VALUE async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 8dcda988e9f..b52953c1a8a 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -205,7 +205,7 @@ class SensorTemplate(TemplateSensor): ) -> None: """Initialize the sensor.""" super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) - self._template = config.get(CONF_STATE) + self._template: template.Template = config[CONF_STATE] if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass diff --git a/mypy.ini b/mypy.ini index b936eab1fd0..d92cca692a4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2701,8 +2701,3 @@ ignore_errors = true [mypy-homeassistant.components.sonos.statistics] ignore_errors = true -[mypy-homeassistant.components.template.number] -ignore_errors = true - -[mypy-homeassistant.components.template.sensor] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index c7bdd8e6c2b..d2c11d4744d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -31,8 +31,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.sensor", "homeassistant.components.sonos.speaker", "homeassistant.components.sonos.statistics", - "homeassistant.components.template.number", - "homeassistant.components.template.sensor", ] # Component modules which should set no_implicit_reexport = true. From bdc63b692bb39f9f13a3db18ea1758a63da43df1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 14 Jul 2022 15:15:51 -0400 Subject: [PATCH 453/820] Bump zigpy from 0.47.2 to 0.47.3 (#75194) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ce1da9071b4..5d5ce7fef12 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.77", "zigpy-deconz==0.18.0", - "zigpy==0.47.2", + "zigpy==0.47.3", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", "zigpy-znp==0.8.1" diff --git a/requirements_all.txt b/requirements_all.txt index e0f109c4a14..790ecd54b81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2522,7 +2522,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.2 +zigpy==0.47.3 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f1b457ade5..a24335f074c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1686,7 +1686,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.2 +zigpy==0.47.3 # homeassistant.components.zwave_js zwave-js-server-python==0.39.0 From 6184f0557d82b43f1649de2bd55f39228bcf4ada Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 21:18:55 +0200 Subject: [PATCH 454/820] Bump pyunifiprotect to 4.0.11 (#75215) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 2410ea91399..815b5250e1d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.0.11", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 790ecd54b81..aa20e5270fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.10 +pyunifiprotect==4.0.11 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a24335f074c..719af4677fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.10 +pyunifiprotect==4.0.11 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 75892385bb656347f6e9b4ea4474eb43b5a8dcf9 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 15 Jul 2022 03:24:24 +0800 Subject: [PATCH 455/820] Fix playback of hls cameras in stream (#75166) --- homeassistant/components/stream/__init__.py | 19 +++-- homeassistant/components/stream/core.py | 39 ++++++++-- homeassistant/components/stream/fmp4utils.py | 12 +++ homeassistant/components/stream/worker.py | 78 ++++++++++++++------ tests/components/stream/test_worker.py | 4 +- 5 files changed, 111 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index b842eb7fb78..ef68ea7bcae 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -57,9 +57,15 @@ from .const import ( SOURCE_TIMEOUT, STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, - TARGET_SEGMENT_DURATION_NON_LL_HLS, ) -from .core import PROVIDERS, IdleTimer, KeyFrameConverter, StreamOutput, StreamSettings +from .core import ( + PROVIDERS, + STREAM_SETTINGS_NON_LL_HLS, + IdleTimer, + KeyFrameConverter, + StreamOutput, + StreamSettings, +) from .diagnostics import Diagnostics from .hls import HlsStreamOutput, async_setup_hls @@ -224,14 +230,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hls_part_timeout=2 * conf[CONF_PART_DURATION], ) else: - hass.data[DOMAIN][ATTR_SETTINGS] = StreamSettings( - ll_hls=False, - min_segment_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS - - SEGMENT_DURATION_ADJUSTER, - part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, - hls_advance_part_limit=3, - hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - ) + hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS # Setup HLS hls_endpoint = async_setup_hls(hass) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 09d9a9d5031..8c456af91aa 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -5,6 +5,7 @@ import asyncio from collections import deque from collections.abc import Callable, Coroutine, Iterable import datetime +import logging from typing import TYPE_CHECKING, Any from aiohttp import web @@ -16,13 +17,20 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry -from .const import ATTR_STREAMS, DOMAIN +from .const import ( + ATTR_STREAMS, + DOMAIN, + SEGMENT_DURATION_ADJUSTER, + TARGET_SEGMENT_DURATION_NON_LL_HLS, +) if TYPE_CHECKING: from av import CodecContext, Packet from . import Stream +_LOGGER = logging.getLogger(__name__) + PROVIDERS: Registry[str, type[StreamOutput]] = Registry() @@ -37,6 +45,15 @@ class StreamSettings: hls_part_timeout: float = attr.ib() +STREAM_SETTINGS_NON_LL_HLS = StreamSettings( + ll_hls=False, + min_segment_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS - SEGMENT_DURATION_ADJUSTER, + part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, + hls_advance_part_limit=3, + hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, +) + + @attr.s(slots=True) class Part: """Represent a segment part.""" @@ -426,12 +443,22 @@ class KeyFrameConverter: return packet = self.packet self.packet = None - # decode packet (flush afterwards) - frames = self._codec_context.decode(packet) - for _i in range(2): - if frames: + for _ in range(2): # Retry once if codec context needs to be flushed + try: + # decode packet (flush afterwards) + frames = self._codec_context.decode(packet) + for _i in range(2): + if frames: + break + frames = self._codec_context.decode(None) break - frames = self._codec_context.decode(None) + except EOFError: + _LOGGER.debug("Codec context needs flushing, attempting to reopen") + self._codec_context.close() + self._codec_context.open() + else: + _LOGGER.debug("Unable to decode keyframe") + return if frames: frame = frames[0] if width and height: diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index f136784cf87..313f5632841 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -2,6 +2,10 @@ from __future__ import annotations from collections.abc import Generator +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from io import BytesIO def find_box( @@ -135,3 +139,11 @@ def get_codec_string(mp4_bytes: bytes) -> str: codecs.append(codec) return ",".join(codecs) + + +def read_init(bytes_io: BytesIO) -> bytes: + """Read the init from a mp4 file.""" + bytes_io.seek(24) + moov_len = int.from_bytes(bytes_io.read(4), byteorder="big") + bytes_io.seek(0) + return bytes_io.read(24 + moov_len) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index e46d83542f7..aa49a6de7ae 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -5,11 +5,12 @@ from collections import defaultdict, deque from collections.abc import Callable, Generator, Iterator, Mapping import contextlib import datetime -from io import BytesIO +from io import SEEK_END, BytesIO import logging from threading import Event from typing import Any, cast +import attr import av from homeassistant.core import HomeAssistant @@ -24,8 +25,16 @@ from .const import ( SEGMENT_CONTAINER_FORMAT, SOURCE_TIMEOUT, ) -from .core import KeyFrameConverter, Part, Segment, StreamOutput, StreamSettings +from .core import ( + STREAM_SETTINGS_NON_LL_HLS, + KeyFrameConverter, + Part, + Segment, + StreamOutput, + StreamSettings, +) from .diagnostics import Diagnostics +from .fmp4utils import read_init from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) @@ -108,7 +117,7 @@ class StreamMuxer: hass: HomeAssistant, video_stream: av.video.VideoStream, audio_stream: av.audio.stream.AudioStream | None, - audio_bsf: av.BitStreamFilterContext | None, + audio_bsf: av.BitStreamFilter | None, stream_state: StreamState, stream_settings: StreamSettings, ) -> None: @@ -120,6 +129,7 @@ class StreamMuxer: self._input_video_stream: av.video.VideoStream = video_stream self._input_audio_stream: av.audio.stream.AudioStream | None = audio_stream self._audio_bsf = audio_bsf + self._audio_bsf_context: av.BitStreamFilterContext = None self._output_video_stream: av.video.VideoStream = None self._output_audio_stream: av.audio.stream.AudioStream | None = None self._segment: Segment | None = None @@ -151,7 +161,7 @@ class StreamMuxer: **{ # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 # "cmaf" flag replaces several of the movflags used, but too recent to use for now - "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer", + "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov", # Sometimes the first segment begins with negative timestamps, and this setting just # adjusts the timestamps in the output from that segment to start from 0. Helps from # having to make some adjustments in test_durations @@ -164,7 +174,7 @@ class StreamMuxer: # Fragment durations may exceed the 15% allowed variance but it seems ok **( { - "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer", + "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov", # Create a fragment every TARGET_PART_DURATION. The data from each fragment is stored in # a "Part" that can be combined with the data from all the other "Part"s, plus an init # section, to reconstitute the data in a "Segment". @@ -194,8 +204,11 @@ class StreamMuxer: # Check if audio is requested output_astream = None if input_astream: + if self._audio_bsf: + self._audio_bsf_context = self._audio_bsf.create() + self._audio_bsf_context.set_input_stream(input_astream) output_astream = container.add_stream( - template=self._audio_bsf or input_astream + template=self._audio_bsf_context or input_astream ) return container, output_vstream, output_astream @@ -238,15 +251,29 @@ class StreamMuxer: self._part_has_keyframe |= packet.is_keyframe elif packet.stream == self._input_audio_stream: - if self._audio_bsf: - self._audio_bsf.send(packet) - while packet := self._audio_bsf.recv(): + if self._audio_bsf_context: + self._audio_bsf_context.send(packet) + while packet := self._audio_bsf_context.recv(): packet.stream = self._output_audio_stream self._av_output.mux(packet) return packet.stream = self._output_audio_stream self._av_output.mux(packet) + def create_segment(self) -> None: + """Create a segment when the moov is ready.""" + self._segment = Segment( + sequence=self._stream_state.sequence, + stream_id=self._stream_state.stream_id, + init=read_init(self._memory_file), + # Fetch the latest StreamOutputs, which may have changed since the + # worker started. + stream_outputs=self._stream_state.outputs, + start_time=self._start_time, + ) + self._memory_file_pos = self._memory_file.tell() + self._memory_file.seek(0, SEEK_END) + def check_flush_part(self, packet: av.Packet) -> None: """Check for and mark a part segment boundary and record its duration.""" if self._memory_file_pos == self._memory_file.tell(): @@ -254,16 +281,10 @@ class StreamMuxer: if self._segment is None: # We have our first non-zero byte position. This means the init has just # been written. Create a Segment and put it to the queue of each output. - self._segment = Segment( - sequence=self._stream_state.sequence, - stream_id=self._stream_state.stream_id, - init=self._memory_file.getvalue(), - # Fetch the latest StreamOutputs, which may have changed since the - # worker started. - stream_outputs=self._stream_state.outputs, - start_time=self._start_time, - ) - self._memory_file_pos = self._memory_file.tell() + self.create_segment() + # When using delay_moov, the moov is not written until a moof is also ready + # Flush the moof + self.flush(packet, last_part=False) else: # These are the ends of the part segments self.flush(packet, last_part=False) @@ -297,6 +318,10 @@ class StreamMuxer: # Closing the av_output will write the remaining buffered data to the # memory_file as a new moof/mdat. self._av_output.close() + # With delay_moov, this may be the first time the file pointer has + # moved, so the segment may not yet have been created + if not self._segment: + self.create_segment() elif not self._part_has_keyframe: # Parts which are not the last part or an independent part should # not have durations below 0.85 of the part target duration. @@ -305,6 +330,9 @@ class StreamMuxer: self._part_start_dts + 0.85 * self._stream_settings.part_target_duration / packet.time_base, ) + # Undo dts adjustments if we don't have ll_hls + if not self._stream_settings.ll_hls: + adjusted_dts = packet.dts assert self._segment self._memory_file.seek(self._memory_file_pos) self._hass.loop.call_soon_threadsafe( @@ -445,10 +473,7 @@ def get_audio_bitstream_filter( _LOGGER.debug( "ADTS AAC detected. Adding aac_adtstoaac bitstream filter" ) - bsf = av.BitStreamFilter("aac_adtstoasc") - bsf_context = bsf.create() - bsf_context.set_input_stream(audio_stream) - return bsf_context + return av.BitStreamFilter("aac_adtstoasc") break return None @@ -489,7 +514,12 @@ def stream_worker( audio_stream = None # Disable ll-hls for hls inputs if container.format.name == "hls": - stream_settings.ll_hls = False + for field in attr.fields(StreamSettings): + setattr( + stream_settings, + field.name, + getattr(STREAM_SETTINGS_NON_LL_HLS, field.name), + ) stream_state.diagnostics.set_value("container_format", container.format.name) stream_state.diagnostics.set_value("video_codec", video_stream.name) if audio_stream: diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index d887a165b44..94d77e7657e 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -755,7 +755,9 @@ async def test_durations(hass, worker_finished_stream): }, ) - source = generate_h264_video(duration=SEGMENT_DURATION + 1) + source = generate_h264_video( + duration=round(SEGMENT_DURATION + target_part_duration + 1) + ) worker_finished, mock_stream = worker_finished_stream with patch("homeassistant.components.stream.Stream", wraps=mock_stream): From e7ae2fada76a43ead67f3e2317d737ee76d4816c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 21:40:39 +0200 Subject: [PATCH 456/820] Remove evohome from mypy ignore list (#75175) --- homeassistant/components/evohome/__init__.py | 57 +++++++++++++------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 27c3698143b..9c46ff43032 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -113,8 +113,9 @@ def _dt_aware_to_naive(dt_aware: dt) -> dt: def convert_until(status_dict: dict, until_key: str) -> None: """Reformat a dt str from "%Y-%m-%dT%H:%M:%SZ" as local/aware/isoformat.""" - if until_key in status_dict: # only present for certain modes - dt_utc_naive = dt_util.parse_datetime(status_dict[until_key]) + if until_key in status_dict and ( # only present for certain modes + dt_utc_naive := dt_util.parse_datetime(status_dict[until_key]) + ): status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() @@ -125,7 +126,9 @@ def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: """Convert a string to snake_case.""" string = re.sub(r"[\-\.\s]", "_", str(key)) return (string[0]).lower() + re.sub( - r"[A-Z]", lambda matched: f"_{matched.group(0).lower()}", string[1:] + r"[A-Z]", + lambda matched: f"_{matched.group(0).lower()}", # type:ignore[str-bytes-safe] + string[1:], ) return { @@ -136,7 +139,7 @@ def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: } -def _handle_exception(err) -> bool: +def _handle_exception(err) -> None: """Return False if the exception can't be ignored.""" try: raise err @@ -190,15 +193,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return ({}, None) # evohomeasync2 requires naive/local datetimes as strings - if tokens.get(ACCESS_TOKEN_EXPIRES) is not None: - tokens[ACCESS_TOKEN_EXPIRES] = _dt_aware_to_naive( - dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) - ) + if tokens.get(ACCESS_TOKEN_EXPIRES) is not None and ( + expires := dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) + ): + tokens[ACCESS_TOKEN_EXPIRES] = _dt_aware_to_naive(expires) user_data = tokens.pop(USER_DATA, None) return (tokens, user_data) - store = Store(hass, STORAGE_VER, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) tokens, user_data = await load_auth_tokens(store) client_v2 = evohomeasync2.EvohomeClient( @@ -230,7 +233,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return False if _LOGGER.isEnabledFor(logging.DEBUG): - _config = {"locationInfo": {"timeZone": None}, GWS: [{TCS: None}]} + _config: dict[str, Any] = { + "locationInfo": {"timeZone": None}, + GWS: [{TCS: None}], + } _config["locationInfo"]["timeZone"] = loc_config["locationInfo"]["timeZone"] _config[GWS][0][TCS] = loc_config[GWS][0][TCS] _LOGGER.debug("Config = %s", _config) @@ -389,7 +395,14 @@ def setup_service_functions(hass: HomeAssistant, broker): class EvoBroker: """Container for evohome client and data.""" - def __init__(self, hass, client, client_v1, store, params) -> None: + def __init__( + self, + hass, + client: evohomeasync2.EvohomeClient, + client_v1: evohomeasync.EvohomeClient | None, + store: Store[dict[str, Any]], + params, + ) -> None: """Initialize the evohome client and its data structure.""" self.hass = hass self.client = client @@ -403,7 +416,7 @@ class EvoBroker: self.tcs_utc_offset = timedelta( minutes=client.locations[loc_idx].timeZone[UTC_OFFSET] ) - self.temps = {} + self.temps: dict[str, Any] | None = {} async def save_auth_tokens(self) -> None: """Save access tokens and session IDs to the store for later use.""" @@ -443,6 +456,8 @@ class EvoBroker: async def _update_v1_api_temps(self, *args, **kwargs) -> None: """Get the latest high-precision temperatures of the default Location.""" + assert self.client_v1 + def get_session_id(client_v1) -> str | None: user_data = client_v1.user_data if client_v1 else None return user_data.get("sessionId") if user_data else None @@ -526,7 +541,7 @@ class EvoDevice(Entity): self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs - self._device_state_attrs = {} + self._device_state_attrs: dict[str, Any] = {} async def async_refresh(self, payload: dict | None = None) -> None: """Process any signals.""" @@ -575,8 +590,8 @@ class EvoChild(EvoDevice): def __init__(self, evo_broker, evo_device) -> None: """Initialize a evohome Controller (hub).""" super().__init__(evo_broker, evo_device) - self._schedule = {} - self._setpoints = {} + self._schedule: dict[str, Any] = {} + self._setpoints: dict[str, Any] = {} @property def current_temperature(self) -> float | None: @@ -590,6 +605,8 @@ class EvoChild(EvoDevice): if self._evo_device.temperatureStatus["isAvailable"]: return self._evo_device.temperatureStatus["temperature"] + return None + @property def setpoints(self) -> dict[str, Any]: """Return the current/next setpoints from the schedule. @@ -630,9 +647,12 @@ class EvoChild(EvoDevice): day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] switchpoint = day["Switchpoints"][idx] + switchpoint_time_of_day = dt_util.parse_datetime( + f"{sp_date}T{switchpoint['TimeOfDay']}" + ) + assert switchpoint_time_of_day dt_aware = _dt_evo_to_aware( - dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}"), - self._evo_broker.tcs_utc_offset, + switchpoint_time_of_day, self._evo_broker.tcs_utc_offset ) self._setpoints[f"{key}_sp_from"] = dt_aware.isoformat() @@ -661,7 +681,8 @@ class EvoChild(EvoDevice): async def async_update(self) -> None: """Get the latest state data.""" next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") - if dt_util.now() >= dt_util.parse_datetime(next_sp_from): + next_sp_from_dt = dt_util.parse_datetime(next_sp_from) + if next_sp_from_dt is None or dt_util.now() >= next_sp_from_dt: await self._update_schedule() # no schedule, or it's out-of-date self._device_state_attrs = {"setpoints": self.setpoints} diff --git a/mypy.ini b/mypy.ini index d92cca692a4..78117ef2b1e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2662,9 +2662,6 @@ ignore_errors = true [mypy-homeassistant.components.cloud.http_api] ignore_errors = true -[mypy-homeassistant.components.evohome] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d2c11d4744d..2e3e80ebc50 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -18,7 +18,6 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", - "homeassistant.components.evohome", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From f0cc565f6c53744db744b88fada0e8f7366e1bc0 Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Thu, 14 Jul 2022 21:40:53 +0200 Subject: [PATCH 457/820] Fix Alexa: Only trigger doorbell event on actual state change to "ON" (#74924) * Alexa: Only trigger doorbell event on actual state change to "ON" * Remove unnecessary check for new_state Co-authored-by: Jan Bouwhuis * First check state is `on` before checking the old state Co-authored-by: Jan Bouwhuis --- .../components/alexa/state_report.py | 4 +- tests/components/alexa/test_state_report.py | 50 ++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index d3e476c2e8c..783397ca047 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -86,7 +86,9 @@ async def async_enable_proactive_mode(hass, smart_home_config): return if should_doorbell: - if new_state.state == STATE_ON: + if new_state.state == STATE_ON and ( + old_state is None or old_state.state != STATE_ON + ): await async_send_doorbell_event_message( hass, smart_home_config, alexa_changed_entity ) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index b2d36b9d179..bb61cea2413 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -346,7 +346,11 @@ async def test_doorbell_event(hass, aioclient_mock): hass.states.async_set( "binary_sensor.test_doorbell", "off", - {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + "linkquality": 42, + }, ) await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) @@ -354,7 +358,21 @@ async def test_doorbell_event(hass, aioclient_mock): hass.states.async_set( "binary_sensor.test_doorbell", "on", - {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + "linkquality": 42, + }, + ) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + "linkquality": 99, + }, ) # To trigger event listener @@ -386,6 +404,34 @@ async def test_doorbell_event(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 2 +async def test_doorbell_event_from_unknown(hass, aioclient_mock): + """Test doorbell press reports.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + }, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa.DoorbellEventSource" + assert call_json["event"]["header"]["name"] == "DoorbellPress" + assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION" + assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell" + + async def test_doorbell_event_fail(hass, aioclient_mock, caplog): """Test proactive state retries once.""" aioclient_mock.post( From 9a4a7e2f4dd2d440df91ac34a0abdd844fbd86f4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 21:43:14 +0200 Subject: [PATCH 458/820] Extend failed login message with the request URL (#75218) --- homeassistant/components/http/ban.py | 2 +- tests/components/http/test_ban.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 81349fe95a1..ee8324b2791 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -117,7 +117,7 @@ async def process_wrong_login(request: Request) -> None: # The user-agent is unsanitized input so we only include it in the log user_agent = request.headers.get("user-agent") - log_msg = f"{base_msg} ({user_agent})" + log_msg = f"{base_msg} Requested URL: '{request.rel_url}'. ({user_agent})" notification_msg = f"{base_msg} See the log for details." diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 05a6493c9c2..7a4202c1a67 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -234,7 +234,7 @@ async def test_ban_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_ip_bans_file_creation(hass, aiohttp_client): +async def test_ip_bans_file_creation(hass, aiohttp_client, caplog): """Testing if banned IP file created.""" app = web.Application() app["hass"] = hass @@ -243,7 +243,7 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): """Return a mock web response.""" raise HTTPUnauthorized - app.router.add_get("/", unauth_handler) + app.router.add_get("/example", unauth_handler) setup_bans(hass, app, 2) mock_real_ip(app)("200.201.202.204") @@ -259,19 +259,19 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): m_open = mock_open() with patch("homeassistant.components.http.ban.open", m_open, create=True): - resp = await client.get("/") + resp = await client.get("/example") assert resp.status == HTTPStatus.UNAUTHORIZED assert len(manager.ip_bans_lookup) == len(BANNED_IPS) assert m_open.call_count == 0 - resp = await client.get("/") + resp = await client.get("/example") assert resp.status == HTTPStatus.UNAUTHORIZED assert len(manager.ip_bans_lookup) == len(BANNED_IPS) + 1 m_open.assert_called_once_with( hass.config.path(IP_BANS_FILE), "a", encoding="utf8" ) - resp = await client.get("/") + resp = await client.get("/example") assert resp.status == HTTPStatus.FORBIDDEN assert m_open.call_count == 1 @@ -283,6 +283,11 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): == "Login attempt or request with invalid authentication from example.com (200.201.202.204). See the log for details." ) + assert ( + "Login attempt or request with invalid authentication from example.com (200.201.202.204). Requested URL: '/example'." + in caplog.text + ) + async def test_failed_login_attempts_counter(hass, aiohttp_client): """Testing if failed login attempts counter increased.""" From 09f37fc522891aa94d110b4a2990bb299870b4c2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 14 Jul 2022 21:46:26 +0200 Subject: [PATCH 459/820] Migrate SQL to new entity naming style (#75203) --- homeassistant/components/sql/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 33ddafe2c0a..dfb1e15f052 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -145,6 +145,7 @@ class SQLSensor(SensorEntity): """Representation of an SQL sensor.""" _attr_icon = "mdi:database-search" + _attr_has_entity_name = True def __init__( self, @@ -157,7 +158,6 @@ class SQLSensor(SensorEntity): entry_id: str, ) -> None: """Initialize the SQL sensor.""" - self._attr_name = name self._query = query self._attr_native_unit_of_measurement = unit self._template = value_template From 72906bf1547b48f2f81aafd418c1c8880046d649 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 14 Jul 2022 15:56:36 -0400 Subject: [PATCH 460/820] Migrate UPB to new entity naming style (#75096) Co-authored-by: Franck Nijhof --- homeassistant/components/upb/__init__.py | 5 ----- homeassistant/components/upb/light.py | 2 ++ homeassistant/components/upb/scene.py | 5 +++++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index b2980ace4e1..5e03df71750 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -75,11 +75,6 @@ class UpbEntity(Entity): element_type = "link" if element.addr.is_link else "device" self._unique_id = f"{unique_id}_{element_type}_{element.addr}" - @property - def name(self): - """Name of the element.""" - return self._element.name - @property def unique_id(self): """Return unique id of the element.""" diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 13f680d9e5a..98a775c18ab 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -49,6 +49,8 @@ async def async_setup_entry( class UpbLight(UpbAttachedEntity, LightEntity): """Representation of an UPB Light.""" + _attr_has_entity_name = True + def __init__(self, element, unique_id, upb): """Initialize an UpbLight.""" super().__init__(element, unique_id, upb) diff --git a/homeassistant/components/upb/scene.py b/homeassistant/components/upb/scene.py index 2334e87aeaa..fe6f07199c4 100644 --- a/homeassistant/components/upb/scene.py +++ b/homeassistant/components/upb/scene.py @@ -49,6 +49,11 @@ async def async_setup_entry( class UpbLink(UpbEntity, Scene): """Representation of an UPB Link.""" + def __init__(self, element, unique_id, upb): + """Initialize the base of all UPB devices.""" + super().__init__(element, unique_id, upb) + self._attr_name = element.name + async def async_activate(self, **kwargs: Any) -> None: """Activate the task.""" self._element.activate() From d004adf8335f0d6118fd80e250119e24bd6aa254 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 22:04:19 +0200 Subject: [PATCH 461/820] Add entity descriptions in AdGuard Home sensors (#75179) --- homeassistant/components/adguard/__init__.py | 8 +- homeassistant/components/adguard/sensor.py | 286 +++++++------------ 2 files changed, 102 insertions(+), 192 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 0c43f84bdfc..e27aef6389d 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -138,8 +138,8 @@ class AdGuardHomeEntity(Entity): self, adguard: AdGuardHome, entry: ConfigEntry, - name: str, - icon: str, + name: str | None, + icon: str | None, enabled_default: bool = True, ) -> None: """Initialize the AdGuard Home entity.""" @@ -151,12 +151,12 @@ class AdGuardHomeEntity(Entity): self.adguard = adguard @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the entity.""" return self._name @property - def icon(self) -> str: + def icon(self) -> str | None: """Return the mdi icon of the entity.""" return self._icon diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 2d3226aba59..19ed0b96801 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -1,11 +1,14 @@ """Support for AdGuard Home sensors.""" from __future__ import annotations +from collections.abc import Callable, Coroutine +from dataclasses import dataclass from datetime import timedelta +from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS from homeassistant.core import HomeAssistant @@ -19,6 +22,81 @@ SCAN_INTERVAL = timedelta(seconds=300) PARALLEL_UPDATES = 4 +@dataclass +class AdGuardHomeEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, int | float]]] + + +@dataclass +class AdGuardHomeEntityDescription( + SensorEntityDescription, AdGuardHomeEntityDescriptionMixin +): + """Describes AdGuard Home sensor entity.""" + + +SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( + AdGuardHomeEntityDescription( + key="dns_queries", + name="DNS queries", + icon="mdi:magnify", + native_unit_of_measurement="queries", + value_fn=lambda adguard: adguard.stats.dns_queries, + ), + AdGuardHomeEntityDescription( + key="blocked_filtering", + name="DNS queries blocked", + icon="mdi:magnify-close", + native_unit_of_measurement="queries", + value_fn=lambda adguard: adguard.stats.blocked_filtering, + ), + AdGuardHomeEntityDescription( + key="blocked_percentage", + name="DNS queries blocked ratio", + icon="mdi:magnify-close", + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda adguard: adguard.stats.blocked_percentage, + ), + AdGuardHomeEntityDescription( + key="blocked_parental", + name="Parental control blocked", + icon="mdi:human-male-girl", + native_unit_of_measurement="requests", + value_fn=lambda adguard: adguard.stats.replaced_parental, + ), + AdGuardHomeEntityDescription( + key="blocked_safebrowsing", + name="Safe browsing blocked", + icon="mdi:shield-half-full", + native_unit_of_measurement="requests", + value_fn=lambda adguard: adguard.stats.replaced_safebrowsing, + ), + AdGuardHomeEntityDescription( + key="enforced_safesearch", + name="Safe searches enforced", + icon="mdi:shield-search", + native_unit_of_measurement="requests", + value_fn=lambda adguard: adguard.stats.replaced_safesearch, + ), + AdGuardHomeEntityDescription( + key="average_speed", + name="Average processing speed", + icon="mdi:speedometer", + native_unit_of_measurement=TIME_MILLISECONDS, + value_fn=lambda adguard: adguard.stats.avg_processing_time, + ), + AdGuardHomeEntityDescription( + key="rules_count", + name="Rules count", + icon="mdi:counter", + native_unit_of_measurement="rules", + value_fn=lambda adguard: adguard.stats.avg_processing_time, + entity_registry_enabled_default=False, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -34,215 +112,47 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version - sensors = [ - AdGuardHomeDNSQueriesSensor(adguard, entry), - AdGuardHomeBlockedFilteringSensor(adguard, entry), - AdGuardHomePercentageBlockedSensor(adguard, entry), - AdGuardHomeReplacedParentalSensor(adguard, entry), - AdGuardHomeReplacedSafeBrowsingSensor(adguard, entry), - AdGuardHomeReplacedSafeSearchSensor(adguard, entry), - AdGuardHomeAverageProcessingTimeSensor(adguard, entry), - AdGuardHomeRulesCountSensor(adguard, entry), - ] - - async_add_entities(sensors, True) + async_add_entities( + [AdGuardHomeSensor(adguard, entry, description) for description in SENSORS], + True, + ) class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): """Defines a AdGuard Home sensor.""" + entity_description: AdGuardHomeEntityDescription + def __init__( self, adguard: AdGuardHome, entry: ConfigEntry, - name: str, - icon: str, - measurement: str, - unit_of_measurement: str, - enabled_default: bool = True, + description: AdGuardHomeEntityDescription, ) -> None: """Initialize AdGuard Home sensor.""" - self._state: int | str | None = None - self._unit_of_measurement = unit_of_measurement - self.measurement = measurement + self.entity_description = description - super().__init__(adguard, entry, name, icon, enabled_default) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return "_".join( + self._attr_unique_id = "_".join( [ DOMAIN, - self.adguard.host, - str(self.adguard.port), + adguard.host, + str(adguard.port), "sensor", - self.measurement, + description.key, ] ) - @property - def native_value(self) -> int | str | None: - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - -class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): - """Defines a AdGuard Home DNS Queries sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" super().__init__( adguard, entry, - "DNS queries", - "mdi:magnify", - "dns_queries", - "queries", + description.name, + description.icon, + description.entity_registry_enabled_default, ) async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.dns_queries() - - -class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): - """Defines a AdGuard Home blocked by filtering sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "DNS queries blocked", - "mdi:magnify-close", - "blocked_filtering", - "queries", - enabled_default=False, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.blocked_filtering() - - -class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): - """Defines a AdGuard Home blocked percentage sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "DNS queries blocked ratio", - "mdi:magnify-close", - "blocked_percentage", - PERCENTAGE, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - percentage = await self.adguard.stats.blocked_percentage() - self._state = f"{percentage:.2f}" - - -class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): - """Defines a AdGuard Home replaced by parental control sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Parental control blocked", - "mdi:human-male-girl", - "blocked_parental", - "requests", - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.replaced_parental() - - -class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): - """Defines a AdGuard Home replaced by safe browsing sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Safe browsing blocked", - "mdi:shield-half-full", - "blocked_safebrowsing", - "requests", - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.replaced_safebrowsing() - - -class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): - """Defines a AdGuard Home replaced by safe search sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Safe searches enforced", - "mdi:shield-search", - "enforced_safesearch", - "requests", - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.replaced_safesearch() - - -class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): - """Defines a AdGuard Home average processing time sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Average processing speed", - "mdi:speedometer", - "average_speed", - TIME_MILLISECONDS, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - average = await self.adguard.stats.avg_processing_time() - self._state = f"{average:.2f}" - - -class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): - """Defines a AdGuard Home rules count sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Rules count", - "mdi:counter", - "rules_count", - "rules", - enabled_default=False, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.filtering.rules_count(allowlist=False) + value = await self.entity_description.value_fn(self.adguard)() + self._attr_native_value = value + if isinstance(value, float): + self._attr_native_value = f"{value:.2f}" From fef1b842ce9ef96303bfe0fdf3e9d10d356ca082 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 22:04:46 +0200 Subject: [PATCH 462/820] Update wled to 0.14.1 (#75174) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wled/test_diagnostics.py | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 668950b9326..2fc00131fac 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.13.2"], + "requirements": ["wled==0.14.1"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index aa20e5270fd..1a41d7b544c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2448,7 +2448,7 @@ wirelesstagpy==0.8.1 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.2 +wled==0.14.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 719af4677fa..4949ab91bbf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1633,7 +1633,7 @@ wiffi==1.1.0 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.2 +wled==0.14.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/tests/components/wled/test_diagnostics.py b/tests/components/wled/test_diagnostics.py index d8782848c92..8f086331f8f 100644 --- a/tests/components/wled/test_diagnostics.py +++ b/tests/components/wled/test_diagnostics.py @@ -26,7 +26,9 @@ async def test_diagnostics( "free_heap": 14600, "leds": { "__type": "", - "repr": "Leds(cct=False, count=30, fps=None, max_power=850, max_segments=10, power=470, rgbw=False, wv=True)", + "repr": "Leds(cct=False, count=30, fps=None, light_capabilities=None, " + "max_power=850, max_segments=10, power=470, rgbw=False, wv=True, " + "segment_light_capabilities=None)", }, "live_ip": "Unknown", "live_mode": "Unknown", From 61cc9f5288f378529daa122062b6120d50d1dffd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 22:06:08 +0200 Subject: [PATCH 463/820] Consolidate executor jobs when loading integration manifests (#75176) --- homeassistant/bootstrap.py | 12 +- .../components/analytics/analytics.py | 14 +- .../components/websocket_api/commands.py | 32 ++-- homeassistant/helpers/service.py | 18 +-- homeassistant/helpers/translation.py | 19 +-- homeassistant/loader.py | 146 +++++++++++------- tests/components/analytics/test_analytics.py | 8 +- tests/helpers/test_translation.py | 12 +- 8 files changed, 143 insertions(+), 118 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 5d19249e37b..b8ec5987142 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -35,7 +35,6 @@ from .setup import ( async_setup_component, ) from .util import dt as dt_util -from .util.async_ import gather_with_concurrency from .util.logging import async_activate_log_queue_handler from .util.package import async_get_user_site, is_virtual_env @@ -479,14 +478,9 @@ async def _async_set_up_integrations( integrations_to_process = [ int_or_exc - for int_or_exc in await gather_with_concurrency( - loader.MAX_LOAD_CONCURRENTLY, - *( - loader.async_get_integration(hass, domain) - for domain in old_to_resolve - ), - return_exceptions=True, - ) + for int_or_exc in ( + await loader.async_get_integrations(hass, old_to_resolve) + ).values() if isinstance(int_or_exc, loader.Integration) ] resolve_dependencies_tasks = [ diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 5bb0368b021..1a696b0c206 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.storage import Store from homeassistant.helpers.system_info import async_get_system_info -from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.loader import IntegrationNotFound, async_get_integrations from homeassistant.setup import async_get_loaded_integrations from .const import ( @@ -182,15 +182,9 @@ class Analytics: if self.preferences.get(ATTR_USAGE, False) or self.preferences.get( ATTR_STATISTICS, False ): - configured_integrations = await asyncio.gather( - *( - async_get_integration(self.hass, domain) - for domain in async_get_loaded_integrations(self.hass) - ), - return_exceptions=True, - ) - - for integration in configured_integrations: + domains = async_get_loaded_integrations(self.hass) + configured_integrations = await async_get_integrations(self.hass, domains) + for integration in configured_integrations.values(): if isinstance(integration, IntegrationNotFound): continue diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b7e7a353633..6c18fd96627 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,7 +1,6 @@ """Commands part of Websocket API.""" from __future__ import annotations -import asyncio from collections.abc import Callable import datetime as dt import json @@ -32,7 +31,12 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.json import JSON_DUMP, ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.loader import ( + Integration, + IntegrationNotFound, + async_get_integration, + async_get_integrations, +) from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations from homeassistant.util.json import ( find_paths_unserializable_data, @@ -372,9 +376,13 @@ async def handle_manifest_list( wanted_integrations = msg.get("integrations") if wanted_integrations is None: wanted_integrations = async_get_loaded_integrations(hass) - integrations = await asyncio.gather( - *(async_get_integration(hass, domain) for domain in wanted_integrations) - ) + + ints_or_excs = await async_get_integrations(hass, wanted_integrations) + integrations: list[Integration] = [] + for int_or_exc in ints_or_excs.values(): + if isinstance(int_or_exc, Exception): + raise int_or_exc + integrations.append(int_or_exc) connection.send_result( msg["id"], [integration.manifest for integration in integrations] ) @@ -706,12 +714,12 @@ async def handle_supported_brands( ) -> None: """Handle supported brands command.""" data = {} - for integration in await asyncio.gather( - *[ - async_get_integration(hass, integration) - for integration in supported_brands.HAS_SUPPORTED_BRANDS - ] - ): - data[integration.domain] = integration.manifest["supported_brands"] + ints_or_excs = await async_get_integrations( + hass, supported_brands.HAS_SUPPORTED_BRANDS + ) + for int_or_exc in ints_or_excs.values(): + if isinstance(int_or_exc, Exception): + raise int_or_exc + data[int_or_exc.domain] = int_or_exc.manifest["supported_brands"] connection.send_result(msg["id"], data) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index bc3451c24c0..cf7fb3b2304 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -31,13 +31,7 @@ from homeassistant.exceptions import ( Unauthorized, UnknownUser, ) -from homeassistant.loader import ( - MAX_LOAD_CONCURRENTLY, - Integration, - async_get_integration, - bind_hass, -) -from homeassistant.util.async_ import gather_with_concurrency +from homeassistant.loader import Integration, async_get_integrations, bind_hass from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml.loader import JSON_TYPE @@ -467,10 +461,12 @@ async def async_get_all_descriptions( loaded = {} if missing: - integrations = await gather_with_concurrency( - MAX_LOAD_CONCURRENTLY, - *(async_get_integration(hass, domain) for domain in missing), - ) + ints_or_excs = await async_get_integrations(hass, missing) + integrations = [ + int_or_exc + for int_or_exc in ints_or_excs.values() + if isinstance(int_or_exc, Integration) + ] contents = await hass.async_add_executor_job( _load_services_files, hass, integrations diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index cda50de535b..616baeeea92 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -9,13 +9,11 @@ from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.loader import ( - MAX_LOAD_CONCURRENTLY, Integration, async_get_config_flows, - async_get_integration, + async_get_integrations, bind_hass, ) -from homeassistant.util.async_ import gather_with_concurrency from homeassistant.util.json import load_json _LOGGER = logging.getLogger(__name__) @@ -151,16 +149,13 @@ async def async_get_component_strings( ) -> dict[str, Any]: """Load translations.""" domains = list({loaded.split(".")[-1] for loaded in components}) - integrations = dict( - zip( - domains, - await gather_with_concurrency( - MAX_LOAD_CONCURRENTLY, - *(async_get_integration(hass, domain) for domain in domains), - ), - ) - ) + integrations: dict[str, Integration] = {} + ints_or_excs = await async_get_integrations(hass, domains) + for domain, int_or_exc in ints_or_excs.items(): + if isinstance(int_or_exc, Exception): + raise int_or_exc + integrations[domain] = int_or_exc translations: dict[str, Any] = {} # Determine paths of missing components/platforms diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 0a65928701b..d0e6189ef96 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -7,7 +7,7 @@ documentation as possible to keep it understandable. from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Iterable from contextlib import suppress import functools as ft import importlib @@ -31,7 +31,6 @@ from .generated.ssdp import SSDP from .generated.usb import USB from .generated.zeroconf import HOMEKIT, ZEROCONF from .helpers.json import JSON_DECODE_EXCEPTIONS, json_loads -from .util.async_ import gather_with_concurrency # Typing imports that create a circular dependency if TYPE_CHECKING: @@ -128,6 +127,7 @@ class Manifest(TypedDict, total=False): version: str codeowners: list[str] loggers: list[str] + supported_brands: dict[str, str] def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: @@ -166,19 +166,15 @@ async def _async_get_custom_components( get_sub_directories, custom_components.__path__ ) - integrations = await gather_with_concurrency( - MAX_LOAD_CONCURRENTLY, - *( - hass.async_add_executor_job( - Integration.resolve_from_root, hass, custom_components, comp.name - ) - for comp in dirs - ), + integrations = await hass.async_add_executor_job( + _resolve_integrations_from_root, + hass, + custom_components, + [comp.name for comp in dirs], ) - return { integration.domain: integration - for integration in integrations + for integration in integrations.values() if integration is not None } @@ -681,59 +677,101 @@ class Integration: return f"" +def _resolve_integrations_from_root( + hass: HomeAssistant, root_module: ModuleType, domains: list[str] +) -> dict[str, Integration]: + """Resolve multiple integrations from root.""" + integrations: dict[str, Integration] = {} + for domain in domains: + try: + integration = Integration.resolve_from_root(hass, root_module, domain) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error loading integration: %s", domain) + else: + if integration: + integrations[domain] = integration + return integrations + + async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration: - """Get an integration.""" + """Get integration.""" + integrations_or_excs = await async_get_integrations(hass, [domain]) + int_or_exc = integrations_or_excs[domain] + if isinstance(int_or_exc, Integration): + return int_or_exc + raise int_or_exc + + +async def async_get_integrations( + hass: HomeAssistant, domains: Iterable[str] +) -> dict[str, Integration | Exception]: + """Get integrations.""" if (cache := hass.data.get(DATA_INTEGRATIONS)) is None: if not _async_mount_config_dir(hass): - raise IntegrationNotFound(domain) + return {domain: IntegrationNotFound(domain) for domain in domains} cache = hass.data[DATA_INTEGRATIONS] = {} - int_or_evt: Integration | asyncio.Event | None = cache.get(domain, _UNDEF) + results: dict[str, Integration | Exception] = {} + needed: dict[str, asyncio.Event] = {} + in_progress: dict[str, asyncio.Event] = {} + for domain in domains: + int_or_evt: Integration | asyncio.Event | None = cache.get(domain, _UNDEF) + if isinstance(int_or_evt, asyncio.Event): + in_progress[domain] = int_or_evt + elif int_or_evt is not _UNDEF: + results[domain] = cast(Integration, int_or_evt) + elif "." in domain: + results[domain] = ValueError(f"Invalid domain {domain}") + else: + needed[domain] = cache[domain] = asyncio.Event() - if isinstance(int_or_evt, asyncio.Event): - await int_or_evt.wait() + if in_progress: + await asyncio.gather(*[event.wait() for event in in_progress.values()]) + for domain in in_progress: + # When we have waited and it's _UNDEF, it doesn't exist + # We don't cache that it doesn't exist, or else people can't fix it + # and then restart, because their config will never be valid. + if (int_or_evt := cache.get(domain, _UNDEF)) is _UNDEF: + results[domain] = IntegrationNotFound(domain) + else: + results[domain] = cast(Integration, int_or_evt) - # When we have waited and it's _UNDEF, it doesn't exist - # We don't cache that it doesn't exist, or else people can't fix it - # and then restart, because their config will never be valid. - if (int_or_evt := cache.get(domain, _UNDEF)) is _UNDEF: - raise IntegrationNotFound(domain) + # First we look for custom components + if needed: + # Instead of using resolve_from_root we use the cache of custom + # components to find the integration. + custom = await async_get_custom_components(hass) + for domain, event in needed.items(): + if integration := custom.get(domain): + results[domain] = cache[domain] = integration + event.set() - if int_or_evt is not _UNDEF: - return cast(Integration, int_or_evt) + for domain in results: + if domain in needed: + del needed[domain] - event = cache[domain] = asyncio.Event() + # Now the rest use resolve_from_root + if needed: + from . import components # pylint: disable=import-outside-toplevel - try: - integration = await _async_get_integration(hass, domain) - except Exception: - # Remove event from cache. - cache.pop(domain) - event.set() - raise + integrations = await hass.async_add_executor_job( + _resolve_integrations_from_root, hass, components, list(needed) + ) + for domain, event in needed.items(): + int_or_exc = integrations.get(domain) + if not int_or_exc: + cache.pop(domain) + results[domain] = IntegrationNotFound(domain) + elif isinstance(int_or_exc, Exception): + cache.pop(domain) + exc = IntegrationNotFound(domain) + exc.__cause__ = int_or_exc + results[domain] = exc + else: + results[domain] = cache[domain] = int_or_exc + event.set() - cache[domain] = integration - event.set() - return integration - - -async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integration: - if "." in domain: - raise ValueError(f"Invalid domain {domain}") - - # Instead of using resolve_from_root we use the cache of custom - # components to find the integration. - if integration := (await async_get_custom_components(hass)).get(domain): - return integration - - from . import components # pylint: disable=import-outside-toplevel - - if integration := await hass.async_add_executor_job( - Integration.resolve_from_root, hass, components, domain - ): - return integration - - raise IntegrationNotFound(domain) + return results class LoaderError(Exception): diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index a18c59f171f..82a61126432 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -269,8 +269,8 @@ async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_moc hass.config.components = ["default_config"] with patch( - "homeassistant.components.analytics.analytics.async_get_integration", - side_effect=IntegrationNotFound("any"), + "homeassistant.components.analytics.analytics.async_get_integrations", + return_value={"any": IntegrationNotFound("any")}, ), patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() @@ -291,8 +291,8 @@ async def test_send_statistics_async_get_integration_unknown_exception( hass.config.components = ["default_config"] with pytest.raises(ValueError), patch( - "homeassistant.components.analytics.analytics.async_get_integration", - side_effect=ValueError, + "homeassistant.components.analytics.analytics.async_get_integrations", + return_value={"any": ValueError()}, ), patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 2e30a649a7b..d993233ac5d 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -135,8 +135,8 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): "homeassistant.helpers.translation.load_translations_files", return_value={"component1": {"title": "world"}}, ), patch( - "homeassistant.helpers.translation.async_get_integration", - return_value=integration, + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component1": integration}, ): translations = await translation.async_get_translations( hass, "en", "title", config_flow=True @@ -164,8 +164,8 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): "homeassistant.helpers.translation.load_translations_files", return_value={"component2": {"title": "world"}}, ), patch( - "homeassistant.helpers.translation.async_get_integration", - return_value=integration, + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component2": integration}, ): translations = await translation.async_get_translations( hass, "en", "title", config_flow=True @@ -212,8 +212,8 @@ async def test_get_translations_while_loading_components(hass): "homeassistant.helpers.translation.load_translations_files", mock_load_translation_files, ), patch( - "homeassistant.helpers.translation.async_get_integration", - return_value=integration, + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component1": integration}, ): tasks = [ translation.async_get_translations(hass, "en", "title") for _ in range(5) From 03e3ebb2380f34943e9ff3feae57855f23863ef3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 22:37:12 +0200 Subject: [PATCH 464/820] Use json_loads by default for the aiohttp helper (#75214) --- homeassistant/helpers/aiohttp_client.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2ef96091d15..f44b59ff077 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -7,7 +7,7 @@ from contextlib import suppress from ssl import SSLContext import sys from types import MappingProxyType -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import aiohttp from aiohttp import web @@ -22,7 +22,11 @@ from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util from .frame import warn_use -from .json import json_dumps +from .json import json_dumps, json_loads + +if TYPE_CHECKING: + from aiohttp.typedefs import JSONDecoder + DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" @@ -35,6 +39,19 @@ SERVER_SOFTWARE = "HomeAssistant/{0} aiohttp/{1} Python/{2[0]}.{2[1]}".format( WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session" +class HassClientResponse(aiohttp.ClientResponse): + """aiohttp.ClientResponse with a json method that uses json_loads by default.""" + + async def json( + self, + *args: Any, + loads: JSONDecoder = json_loads, + **kwargs: Any, + ) -> Any: + """Send a json request and parse the json response.""" + return await super().json(*args, loads=loads, **kwargs) + + @callback @bind_hass def async_get_clientsession( @@ -99,6 +116,7 @@ def _async_create_clientsession( clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), json_serialize=json_dumps, + response_class=HassClientResponse, **kwargs, ) # Prevent packages accidentally overriding our default headers From ff297cb90279b942e949f85cead0b60dc725e591 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 14 Jul 2022 22:51:48 +0100 Subject: [PATCH 465/820] Bump aiohomekit to 1.0.0 (#75198) * Bump to 1.0.0rc1 * 1.0.0rc2 * fix one of the tests * simplify test * 1.0.0 Co-authored-by: J. Nick Koston --- .../components/homekit_controller/__init__.py | 2 +- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_init.py | 63 ++++--------------- 5 files changed, 16 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index bc911b991ca..461d46eed1d 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -227,7 +227,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await conn.async_setup(): del hass.data[KNOWN_DEVICES][conn.unique_id] - if (connection := getattr(conn.pairing, "connection")) and hasattr( + if (connection := getattr(conn.pairing, "connection", None)) and hasattr( connection, "host" ): raise ConfigEntryNotReady( diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a83d6264603..0275afa07fe 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.22"], + "requirements": ["aiohomekit==1.0.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 1a41d7b544c..74f8e7fb0c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.22 +aiohomekit==1.0.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4949ab91bbf..1d4cb752271 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.22 +aiohomekit==1.0.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 841b726ac0e..57b39076aa6 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -3,11 +3,11 @@ from datetime import timedelta from unittest.mock import patch -from aiohomekit import AccessoryDisconnectedError, exceptions +from aiohomekit import AccessoryDisconnectedError from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from aiohomekit.testing import FakeController, FakeDiscovery, FakePairing +from aiohomekit.testing import FakePairing from homeassistant.components.homekit_controller.const import DOMAIN, ENTITY_MAP from homeassistant.config_entries import ConfigEntryState @@ -98,7 +98,7 @@ async def test_device_remove_devices(hass, hass_ws_client): ) -async def test_offline_device_raises(hass): +async def test_offline_device_raises(hass, controller): """Test an offline device raises ConfigEntryNotReady.""" is_connected = False @@ -114,56 +114,17 @@ async def test_offline_device_raises(hass): def get_characteristics(self, chars, *args, **kwargs): raise AccessoryDisconnectedError("any") - class OfflineFakeDiscovery(FakeDiscovery): - """Fake discovery that returns an offline pairing.""" - - async def start_pairing(self, alias: str): - if self.description.id in self.controller.pairings: - raise exceptions.AlreadyPairedError( - f"{self.description.id} already paired" - ) - - async def finish_pairing(pairing_code): - if pairing_code != self.pairing_code: - raise exceptions.AuthenticationError("M4") - pairing_data = {} - pairing_data["AccessoryIP"] = self.info["address"] - pairing_data["AccessoryPort"] = self.info["port"] - pairing_data["Connection"] = "IP" - - obj = self.controller.pairings[alias] = OfflineFakePairing( - self.controller, pairing_data, self.accessories - ) - return obj - - return finish_pairing - - class OfflineFakeController(FakeController): - """Fake controller that always returns a discovery with a pairing that always returns False for is_connected.""" - - def add_device(self, accessories): - device_id = "00:00:00:00:00:00" - discovery = self.discoveries[device_id] = OfflineFakeDiscovery( - self, - device_id, - accessories=accessories, - ) - return discovery - - with patch( - "homeassistant.components.homekit_controller.utils.Controller" - ) as controller: - fake_controller = controller.return_value = OfflineFakeController() + with patch("aiohomekit.testing.FakePairing", OfflineFakePairing): await async_setup_component(hass, DOMAIN, {}) + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) - accessory = Accessory.create_with_info( - "TestDevice", "example.com", "Test", "0001", "0.1" - ) - create_alive_service(accessory) - - config_entry, _ = await setup_test_accessories_with_controller( - hass, [accessory], fake_controller - ) + config_entry, _ = await setup_test_accessories_with_controller( + hass, [accessory], controller + ) + await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.SETUP_RETRY From 4a3d047dffa9bbb49ddeb4038f3592e96183ccf1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 14 Jul 2022 23:53:09 +0200 Subject: [PATCH 466/820] Use pydeconz interface controls for fans (#75156) --- homeassistant/components/deconz/fan.py | 67 ++++++++++--------- homeassistant/components/deconz/light.py | 8 +-- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 39 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index ab9a1ba6f4a..6002e61d326 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -1,17 +1,10 @@ """Support for deCONZ fans.""" from __future__ import annotations -from typing import Any, Literal +from typing import Any from pydeconz.models.event import EventType -from pydeconz.models.light.fan import ( - FAN_SPEED_25_PERCENT, - FAN_SPEED_50_PERCENT, - FAN_SPEED_75_PERCENT, - FAN_SPEED_100_PERCENT, - FAN_SPEED_OFF, - Fan, -) +from pydeconz.models.light.light import Light, LightFanSpeed from homeassistant.components.fan import DOMAIN, FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry @@ -25,11 +18,11 @@ from homeassistant.util.percentage import ( from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -ORDERED_NAMED_FAN_SPEEDS: list[Literal[0, 1, 2, 3, 4, 5, 6]] = [ - FAN_SPEED_25_PERCENT, - FAN_SPEED_50_PERCENT, - FAN_SPEED_75_PERCENT, - FAN_SPEED_100_PERCENT, +ORDERED_NAMED_FAN_SPEEDS: list[LightFanSpeed] = [ + LightFanSpeed.PERCENT_25, + LightFanSpeed.PERCENT_50, + LightFanSpeed.PERCENT_75, + LightFanSpeed.PERCENT_100, ] @@ -45,12 +38,14 @@ async def async_setup_entry( @callback def async_add_fan(_: EventType, fan_id: str) -> None: """Add fan from deCONZ.""" - fan = gateway.api.lights.fans[fan_id] + fan = gateway.api.lights.lights[fan_id] + if not fan.supports_fan_speed: + return async_add_entities([DeconzFan(fan, gateway)]) gateway.register_platform_add_device_callback( async_add_fan, - gateway.api.lights.fans, + gateway.api.lights.lights, ) @@ -58,33 +53,32 @@ class DeconzFan(DeconzDevice, FanEntity): """Representation of a deCONZ fan.""" TYPE = DOMAIN - _device: Fan - _default_on_speed: Literal[0, 1, 2, 3, 4, 5, 6] + _device: Light + _default_on_speed = LightFanSpeed.PERCENT_50 _attr_supported_features = FanEntityFeature.SET_SPEED - def __init__(self, device: Fan, gateway: DeconzGateway) -> None: + def __init__(self, device: Light, gateway: DeconzGateway) -> None: """Set up fan.""" super().__init__(device, gateway) - self._default_on_speed = FAN_SPEED_50_PERCENT - if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: - self._default_on_speed = self._device.speed + if device.fan_speed in ORDERED_NAMED_FAN_SPEEDS: + self._default_on_speed = device.fan_speed @property def is_on(self) -> bool: """Return true if fan is on.""" - return self._device.speed != FAN_SPEED_OFF + return self._device.fan_speed != LightFanSpeed.OFF @property def percentage(self) -> int | None: """Return the current speed percentage.""" - if self._device.speed == FAN_SPEED_OFF: + if self._device.fan_speed == LightFanSpeed.OFF: return 0 - if self._device.speed not in ORDERED_NAMED_FAN_SPEEDS: + if self._device.fan_speed not in ORDERED_NAMED_FAN_SPEEDS: return None return ordered_list_item_to_percentage( - ORDERED_NAMED_FAN_SPEEDS, self._device.speed + ORDERED_NAMED_FAN_SPEEDS, self._device.fan_speed ) @property @@ -95,16 +89,19 @@ class DeconzFan(DeconzDevice, FanEntity): @callback def async_update_callback(self) -> None: """Store latest configured speed from the device.""" - if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: - self._default_on_speed = self._device.speed + if self._device.fan_speed in ORDERED_NAMED_FAN_SPEEDS: + self._default_on_speed = self._device.fan_speed super().async_update_callback() async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" if percentage == 0: return await self.async_turn_off() - await self._device.set_speed( - percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + fan_speed=percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ), ) async def async_turn_on( @@ -117,8 +114,14 @@ class DeconzFan(DeconzDevice, FanEntity): if percentage is not None: await self.async_set_percentage(percentage) return - await self._device.set_speed(self._default_on_speed) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + fan_speed=self._default_on_speed, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off fan.""" - await self._device.set_speed(FAN_SPEED_OFF) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + fan_speed=LightFanSpeed.OFF, + ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index e73314aa477..7be6551cccc 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -85,8 +85,7 @@ async def async_setup_entry( @callback def async_add_light(_: EventType, light_id: str) -> None: """Add light from deCONZ.""" - light = gateway.api.lights[light_id] - assert isinstance(light, Light) + light = gateway.api.lights.lights[light_id] if light.type in POWER_PLUGS: return @@ -97,11 +96,6 @@ async def async_setup_entry( gateway.api.lights.lights, ) - gateway.register_platform_add_device_callback( - async_add_light, - gateway.api.lights.fans, - ) - @callback def async_add_group(_: EventType, group_id: str) -> None: """Add group from deCONZ. diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 2ae400bbe19..c1b0b07de02 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==98"], + "requirements": ["pydeconz==99"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 74f8e7fb0c0..98af8acf5e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==98 +pydeconz==99 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d4cb752271..0a1afc49dce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==98 +pydeconz==99 # homeassistant.components.dexcom pydexcom==0.2.3 From ea6bb370a64953dbf29db6c9b4fce902e619e2da Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 14 Jul 2022 18:12:48 -0500 Subject: [PATCH 467/820] Bump frontend to 20220707.1 (#75232) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- mypy.ini | 1 - requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 5c7fd1a20be..288b4796e0e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220707.0"], + "requirements": ["home-assistant-frontend==20220707.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 09eb66c5f34..d19b95fa2d3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 -home-assistant-frontend==20220707.0 +home-assistant-frontend==20220707.1 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/mypy.ini b/mypy.ini index 78117ef2b1e..ab83c443d5a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2697,4 +2697,3 @@ ignore_errors = true [mypy-homeassistant.components.sonos.statistics] ignore_errors = true - diff --git a/requirements_all.txt b/requirements_all.txt index 98af8acf5e4..a695d12c975 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -831,7 +831,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.0 +home-assistant-frontend==20220707.1 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a1afc49dce..5124f58a11c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -601,7 +601,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.0 +home-assistant-frontend==20220707.1 # homeassistant.components.home_connect homeconnect==0.7.1 From 08a361dab9303501e796a679a4bc93b6c342a38b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 15 Jul 2022 00:28:00 +0000 Subject: [PATCH 468/820] [ci skip] Translation update --- .../components/here_travel_time/translations/hu.json | 7 +++++++ .../components/rhasspy/translations/hu.json | 12 ++++++++++++ .../components/rhasspy/translations/id.json | 12 ++++++++++++ .../components/rhasspy/translations/pl.json | 12 ++++++++++++ .../components/withings/translations/hu.json | 4 ++++ .../components/withings/translations/id.json | 4 ++++ 6 files changed, 51 insertions(+) create mode 100644 homeassistant/components/rhasspy/translations/hu.json create mode 100644 homeassistant/components/rhasspy/translations/id.json create mode 100644 homeassistant/components/rhasspy/translations/pl.json diff --git a/homeassistant/components/here_travel_time/translations/hu.json b/homeassistant/components/here_travel_time/translations/hu.json index cdd40f139d7..b3cee83f662 100644 --- a/homeassistant/components/here_travel_time/translations/hu.json +++ b/homeassistant/components/here_travel_time/translations/hu.json @@ -39,6 +39,13 @@ }, "title": "Eredet kiv\u00e1laszt\u00e1sa" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "T\u00e9rk\u00e9pes hely haszn\u00e1lata", + "origin_entity": "Egy entit\u00e1s haszn\u00e1lata" + }, + "title": "V\u00e1lassza az eredetit" + }, "user": { "data": { "api_key": "API kulcs", diff --git a/homeassistant/components/rhasspy/translations/hu.json b/homeassistant/components/rhasspy/translations/hu.json new file mode 100644 index 00000000000..8297d6af059 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva. Csak egyetlen konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "Szeretn\u00e9 enged\u00e9lyezni a Rhasspy t\u00e1mogat\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/id.json b/homeassistant/components/rhasspy/translations/id.json new file mode 100644 index 00000000000..7a0b5445bcd --- /dev/null +++ b/homeassistant/components/rhasspy/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin mengaktifkan dukungan Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/pl.json b/homeassistant/components/rhasspy/translations/pl.json new file mode 100644 index 00000000000..79db07a077f --- /dev/null +++ b/homeassistant/components/rhasspy/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Czy chcesz w\u0142\u0105czy\u0107 obs\u0142ug\u0119 Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index 7504c36c58e..a157d5e3688 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -27,6 +27,10 @@ "reauth": { "description": "A \u201e{profile}\u201d profilt \u00fajra hiteles\u00edteni kell, hogy tov\u00e1bbra is fogadni tudja a Withings adatokat.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "reauth_confirm": { + "description": "A \u201e{profile}\u201d profilt \u00fajra kell hiteles\u00edteni, hogy tov\u00e1bbra is megkaphassa a Withings-adatokat.", + "title": "Az integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } } diff --git a/homeassistant/components/withings/translations/id.json b/homeassistant/components/withings/translations/id.json index eb21a0d3352..4b4490a617b 100644 --- a/homeassistant/components/withings/translations/id.json +++ b/homeassistant/components/withings/translations/id.json @@ -27,6 +27,10 @@ "reauth": { "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.", "title": "Autentikasi Ulang Integrasi" + }, + "reauth_confirm": { + "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.", + "title": "Autentikasi Ulang Integrasi" } } } From 700081e160306c57c1315fc612ee9b4aaa7a07e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Jul 2022 06:09:11 +0200 Subject: [PATCH 469/820] Add entity descriptions in AdGuard Home switches (#75229) --- homeassistant/components/adguard/switch.py | 257 ++++++++------------- 1 file changed, 91 insertions(+), 166 deletions(-) diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 5b4017d4054..efbd5c7fa38 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -1,13 +1,15 @@ """Support for AdGuard Home switches.""" from __future__ import annotations +from collections.abc import Callable, Coroutine +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -22,6 +24,74 @@ SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 1 +@dataclass +class AdGuardHomeSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + is_on_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, bool]]] + turn_on_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, None]]] + turn_off_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, None]]] + + +@dataclass +class AdGuardHomeSwitchEntityDescription( + SwitchEntityDescription, AdGuardHomeSwitchEntityDescriptionMixin +): + """Describes AdGuard Home switch entity.""" + + +SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = ( + AdGuardHomeSwitchEntityDescription( + key="protection", + name="Protection", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.protection_enabled, + turn_on_fn=lambda adguard: adguard.enable_protection, + turn_off_fn=lambda adguard: adguard.disable_protection, + ), + AdGuardHomeSwitchEntityDescription( + key="parental", + name="Parental control", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.parental.enabled, + turn_on_fn=lambda adguard: adguard.parental.enable, + turn_off_fn=lambda adguard: adguard.parental.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="safesearch", + name="Safe search", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.safesearch.enabled, + turn_on_fn=lambda adguard: adguard.safesearch.enable, + turn_off_fn=lambda adguard: adguard.safesearch.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="safebrowsing", + name="Safe browsing", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.safebrowsing.enabled, + turn_on_fn=lambda adguard: adguard.safebrowsing.enable, + turn_off_fn=lambda adguard: adguard.safebrowsing.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="filtering", + name="Filtering", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.filtering.enabled, + turn_on_fn=lambda adguard: adguard.filtering.enable, + turn_off_fn=lambda adguard: adguard.filtering.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="querylog", + name="Query log", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.querylog.enabled, + turn_on_fn=lambda adguard: adguard.querylog.enable, + turn_off_fn=lambda adguard: adguard.querylog.disable, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -37,199 +107,54 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version - switches = [ - AdGuardHomeProtectionSwitch(adguard, entry), - AdGuardHomeFilteringSwitch(adguard, entry), - AdGuardHomeParentalSwitch(adguard, entry), - AdGuardHomeSafeBrowsingSwitch(adguard, entry), - AdGuardHomeSafeSearchSwitch(adguard, entry), - AdGuardHomeQueryLogSwitch(adguard, entry), - ] - async_add_entities(switches, True) + async_add_entities( + [AdGuardHomeSwitch(adguard, entry, description) for description in SWITCHES], + True, + ) class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): """Defines a AdGuard Home switch.""" + entity_description: AdGuardHomeSwitchEntityDescription + def __init__( self, adguard: AdGuardHome, entry: ConfigEntry, - name: str, - icon: str, - key: str, - enabled_default: bool = True, + description: AdGuardHomeSwitchEntityDescription, ) -> None: """Initialize AdGuard Home switch.""" - self._state = False - self._key = key - super().__init__(adguard, entry, name, icon, enabled_default) + self.entity_description = description - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return "_".join( - [DOMAIN, self.adguard.host, str(self.adguard.port), "switch", self._key] + self._attr_unique_id = "_".join( + [DOMAIN, adguard.host, str(adguard.port), "switch", description.key] ) - @property - def is_on(self) -> bool: - """Return the state of the switch.""" - return self._state + super().__init__( + adguard, + entry, + description.name, + description.icon, + description.entity_registry_enabled_default, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" try: - await self._adguard_turn_off() + await self.entity_description.turn_off_fn(self.adguard)() except AdGuardHomeError: _LOGGER.error("An error occurred while turning off AdGuard Home switch") self._available = False - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - raise NotImplementedError() - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" try: - await self._adguard_turn_on() + await self.entity_description.turn_on_fn(self.adguard)() except AdGuardHomeError: _LOGGER.error("An error occurred while turning on AdGuard Home switch") self._available = False - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - raise NotImplementedError() - - -class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home protection switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__(adguard, entry, "Protection", "mdi:shield-check", "protection") - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.disable_protection() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.enable_protection() - async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - self._state = await self.adguard.protection_enabled() - - -class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home parental control switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "Parental control", "mdi:shield-check", "parental" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.parental.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.parental.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.parental.enabled() - - -class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home safe search switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "Safe search", "mdi:shield-check", "safesearch" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.safesearch.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.safesearch.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.safesearch.enabled() - - -class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home safe search switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "Safe browsing", "mdi:shield-check", "safebrowsing" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.safebrowsing.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.safebrowsing.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.safebrowsing.enabled() - - -class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home filtering switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__(adguard, entry, "Filtering", "mdi:shield-check", "filtering") - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.filtering.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.filtering.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.filtering.enabled() - - -class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home query log switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, - entry, - "Query log", - "mdi:shield-check", - "querylog", - enabled_default=False, - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.querylog.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.querylog.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.querylog.enabled() + self._attr_is_on = await self.entity_description.is_on_fn(self.adguard)() From 98807f7efcf7c9327bb46b7628fd657fe4447c6b Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 15 Jul 2022 00:28:00 -0400 Subject: [PATCH 470/820] Bump AIOAladdinConnect to 0.1.25 (#75235) Co-authored-by: Paulus Schoutsen --- homeassistant/components/aladdin_connect/__init__.py | 6 ++++-- homeassistant/components/aladdin_connect/config_flow.py | 7 +++++-- homeassistant/components/aladdin_connect/const.py | 1 + homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 036f5364ef5..40dd0dd981a 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CLIENT_ID, DOMAIN _LOGGER: Final = logging.getLogger(__name__) @@ -23,7 +23,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up platform from a ConfigEntry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - acc = AladdinConnectClient(username, password, async_get_clientsession(hass)) + acc = AladdinConnectClient( + username, password, async_get_clientsession(hass), CLIENT_ID + ) try: if not await acc.login(): raise ConfigEntryAuthFailed("Incorrect Password") diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 0d45ea9a8ef..44d3f01a9ec 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -18,7 +18,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CLIENT_ID, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ acc = AladdinConnectClient( - data[CONF_USERNAME], data[CONF_PASSWORD], async_get_clientsession(hass) + data[CONF_USERNAME], + data[CONF_PASSWORD], + async_get_clientsession(hass), + CLIENT_ID, ) login = await acc.login() await acc.close() diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index 7a11cf63a9e..46d5d468f71 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -18,3 +18,4 @@ STATES_MAP: Final[dict[str, str]] = { DOMAIN = "aladdin_connect" SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE +CLIENT_ID = "1000" diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 8ef86a76bf8..5baeba33971 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.24"], + "requirements": ["AIOAladdinConnect==0.1.25"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index a695d12c975..26a27816bc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.24 +AIOAladdinConnect==0.1.25 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5124f58a11c..df934644fed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.24 +AIOAladdinConnect==0.1.25 # homeassistant.components.adax Adax-local==0.1.4 From 2dde3d02cc88b0f71a95519d9ec722ef620b372c Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 14 Jul 2022 22:29:11 -0600 Subject: [PATCH 471/820] Bump pylitterbot to 2022.7.0 (#75241) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index a07f13a47b5..104a06afc8f 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.3.0"], + "requirements": ["pylitterbot==2022.7.0"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling", "loggers": ["pylitterbot"] diff --git a/requirements_all.txt b/requirements_all.txt index 26a27816bc4..cf89a5622bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1628,7 +1628,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.3.0 +pylitterbot==2022.7.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df934644fed..009ccb04343 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1107,7 +1107,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.3.0 +pylitterbot==2022.7.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 From b4003713b67d3e83397382f4af5d10e8d9a94660 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 14 Jul 2022 22:01:18 -0700 Subject: [PATCH 472/820] Remove nest mac prefix that matches cast devices (#75108) --- homeassistant/components/nest/manifest.json | 3 +-- homeassistant/generated/dhcp.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index d0588d46f06..72e0aed8420 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -11,8 +11,7 @@ "dhcp": [ { "macaddress": "18B430*" }, { "macaddress": "641666*" }, - { "macaddress": "D8EB46*" }, - { "macaddress": "1C53F9*" } + { "macaddress": "D8EB46*" } ], "iot_class": "cloud_push", "loggers": ["google_nest_sdm", "nest"] diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 57be9a62138..c062870f3e3 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -69,7 +69,6 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'nest', 'macaddress': '18B430*'}, {'domain': 'nest', 'macaddress': '641666*'}, {'domain': 'nest', 'macaddress': 'D8EB46*'}, - {'domain': 'nest', 'macaddress': '1C53F9*'}, {'domain': 'nexia', 'hostname': 'xl857-*', 'macaddress': '000231*'}, {'domain': 'nuheat', 'hostname': 'nuheat', 'macaddress': '002338*'}, {'domain': 'nuki', 'hostname': 'nuki_bridge_*'}, From 3e98ac180c4342eaddfa3c3b0226b4d17cb5df94 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Jul 2022 10:44:29 +0200 Subject: [PATCH 473/820] Migrate Trafikverket Ferry to new entity naming style (#75206) --- .../components/trafikverket_ferry/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/trafikverket_ferry/sensor.py b/homeassistant/components/trafikverket_ferry/sensor.py index 256341a7132..b02c673f698 100644 --- a/homeassistant/components/trafikverket_ferry/sensor.py +++ b/homeassistant/components/trafikverket_ferry/sensor.py @@ -51,7 +51,7 @@ class TrafikverketSensorEntityDescription( SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="departure_time", - name="Departure Time", + name="Departure time", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time"]), @@ -59,21 +59,21 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="departure_from", - name="Departure From", + name="Departure from", icon="mdi:ferry", value_fn=lambda data: cast(str, data["departure_from"]), info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_to", - name="Departure To", + name="Departure to", icon="mdi:ferry", value_fn=lambda data: cast(str, data["departure_to"]), info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_modified", - name="Departure Modified", + name="Departure modified", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_modified"]), @@ -82,7 +82,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="departure_time_next", - name="Departure Time Next", + name="Departure time next", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next"]), @@ -91,7 +91,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="departure_time_next_next", - name="Departure Time Next After", + name="Departure time next after", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next_next"]), @@ -121,6 +121,7 @@ class FerrySensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): entity_description: TrafikverketSensorEntityDescription _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -131,7 +132,6 @@ class FerrySensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._attr_name = f"{name} {entity_description.name}" self._attr_unique_id = f"{entry_id}-{entity_description.key}" self.entity_description = entity_description self._attr_device_info = DeviceInfo( From 911402e7472c1a8ca6cde60b3039d21c2e64cec9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Jul 2022 10:47:30 +0200 Subject: [PATCH 474/820] Remove cloud from mypy ignore list (#74449) --- homeassistant/components/cloud/http_api.py | 1 + homeassistant/components/webhook/__init__.py | 18 +++++++++++++----- mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 6086cef703a..8e5c214b388 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -419,6 +419,7 @@ async def websocket_hook_delete(hass, connection, msg): async def _account_data(hass: HomeAssistant, cloud: Cloud): """Generate the auth data JSON response.""" + assert hass.config.api if not cloud.is_logged_in: return { "logged_in": False, diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index fb9927f1b37..449de006bf9 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -6,7 +6,9 @@ from http import HTTPStatus from ipaddress import ip_address import logging import secrets +from typing import TYPE_CHECKING, Any +from aiohttp import StreamReader from aiohttp.web import Request, Response import voluptuous as vol @@ -17,7 +19,7 @@ from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import network -from homeassistant.util.aiohttp import MockRequest, serialize_response +from homeassistant.util.aiohttp import MockRequest, MockStreamReader, serialize_response _LOGGER = logging.getLogger(__name__) @@ -83,17 +85,20 @@ def async_generate_path(webhook_id: str) -> str: @bind_hass async def async_handle_webhook( - hass: HomeAssistant, webhook_id: str, request: Request + hass: HomeAssistant, webhook_id: str, request: Request | MockRequest ) -> Response: """Handle a webhook.""" - handlers = hass.data.setdefault(DOMAIN, {}) + handlers: dict[str, dict[str, Any]] = hass.data.setdefault(DOMAIN, {}) # Always respond successfully to not give away if a hook exists or not. if (webhook := handlers.get(webhook_id)) is None: + content_stream: StreamReader | MockStreamReader if isinstance(request, MockRequest): received_from = request.mock_source + content_stream = request.content else: received_from = request.remote + content_stream = request.content _LOGGER.info( "Received message for unregistered webhook %s from %s", @@ -102,13 +107,16 @@ async def async_handle_webhook( ) # Look at content to provide some context for received webhook # Limit to 64 chars to avoid flooding the log - content = await request.content.read(64) + content = await content_stream.read(64) _LOGGER.debug("%s", content) return Response(status=HTTPStatus.OK) if webhook["local_only"]: + if TYPE_CHECKING: + assert isinstance(request, Request) + assert request.remote is not None try: - remote = ip_address(request.remote) # type: ignore[arg-type] + remote = ip_address(request.remote) except ValueError: _LOGGER.debug("Unable to parse remote ip %s", request.remote) return Response(status=HTTPStatus.OK) diff --git a/mypy.ini b/mypy.ini index ab83c443d5a..5a3cbd7a05f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2656,12 +2656,6 @@ no_implicit_optional = false warn_return_any = false warn_unreachable = false -[mypy-homeassistant.components.cloud.client] -ignore_errors = true - -[mypy-homeassistant.components.cloud.http_api] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 2e3e80ebc50..4bf8cfd68cf 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,8 +16,6 @@ from .model import Config, Integration # remove your component from this list to enable type checks. # Do your best to not add anything new here. IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.cloud.client", - "homeassistant.components.cloud.http_api", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From cba3c8cf65f984849e93e866826f7188e0bd7221 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Jul 2022 10:51:18 +0200 Subject: [PATCH 475/820] Migrate Sensibo to new entity naming style (#75212) --- .../components/sensibo/binary_sensor.py | 17 ++++++----------- homeassistant/components/sensibo/button.py | 3 +-- homeassistant/components/sensibo/climate.py | 1 - homeassistant/components/sensibo/entity.py | 8 +++++--- homeassistant/components/sensibo/number.py | 1 - homeassistant/components/sensibo/select.py | 3 +-- homeassistant/components/sensibo/sensor.py | 16 ++++++---------- homeassistant/components/sensibo/switch.py | 1 - homeassistant/components/sensibo/update.py | 3 +-- 9 files changed, 20 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index ed280aab4fe..3a61570701d 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -55,7 +55,7 @@ class SensiboDeviceBinarySensorEntityDescription( FILTER_CLEAN_REQUIRED_DESCRIPTION = SensiboDeviceBinarySensorEntityDescription( key="filter_clean", device_class=BinarySensorDeviceClass.PROBLEM, - name="Filter Clean Required", + name="Filter clean required", value_fn=lambda data: data.filter_clean, ) @@ -71,7 +71,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( SensiboMotionBinarySensorEntityDescription( key="is_main_sensor", entity_category=EntityCategory.DIAGNOSTIC, - name="Main Sensor", + name="Main sensor", icon="mdi:connection", value_fn=lambda data: data.is_main_sensor, ), @@ -88,7 +88,7 @@ MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, .. SensiboDeviceBinarySensorEntityDescription( key="room_occupied", device_class=BinarySensorDeviceClass.MOTION, - name="Room Occupied", + name="Room occupied", icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, ), @@ -111,7 +111,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( key="pure_geo_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with Presence", + name="Pure Boost linked with presence", icon="mdi:connection", value_fn=lambda data: data.pure_geo_integration, ), @@ -119,7 +119,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( key="pure_measure_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with Indoor Air Quality", + name="Pure Boost linked with indoor air quality", icon="mdi:connection", value_fn=lambda data: data.pure_measure_integration, ), @@ -127,7 +127,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( key="pure_prime_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with Outdoor Air Quality", + name="Pure Boost linked with outdoor air quality", icon="mdi:connection", value_fn=lambda data: data.pure_prime_integration, ), @@ -194,13 +194,9 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, BinarySensorEntity): device_id, sensor_id, sensor_data, - entity_description.name, ) self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" - self._attr_name = ( - f"{self.device_data.name} Motion Sensor {entity_description.name}" - ) @property def is_on(self) -> bool | None: @@ -228,7 +224,6 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, BinarySensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/sensibo/button.py b/homeassistant/components/sensibo/button.py index 97ae6321f7e..ad8a525aebb 100644 --- a/homeassistant/components/sensibo/button.py +++ b/homeassistant/components/sensibo/button.py @@ -16,7 +16,7 @@ PARALLEL_UPDATES = 0 DEVICE_BUTTON_TYPES: ButtonEntityDescription = ButtonEntityDescription( key="reset_filter", - name="Reset Filter", + name="Reset filter", icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, ) @@ -57,7 +57,6 @@ class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index b4af38ab69c..25ac73bfd4f 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -126,7 +126,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Initiate SensiboClimate.""" super().__init__(coordinator, device_id) self._attr_unique_id = device_id - self._attr_name = self.device_data.name self._attr_temperature_unit = ( TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT ) diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index ac2ec24fac1..41d8b8b5070 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -37,6 +37,8 @@ class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): class SensiboDeviceBaseEntity(SensiboBaseEntity): """Representation of a Sensibo device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SensiboDataUpdateCoordinator, @@ -114,21 +116,21 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): class SensiboMotionBaseEntity(SensiboBaseEntity): """Representation of a Sensibo motion entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str, sensor_id: str, sensor_data: MotionSensor, - name: str | None, ) -> None: """Initiate Sensibo Number.""" super().__init__(coordinator, device_id) self._sensor_id = sensor_id - self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, sensor_id)}, - name=f"{self.device_data.name} Motion Sensor {name}", + name=f"{self.device_data.name} Motion Sensor", via_device=(DOMAIN, device_id), manufacturer="Sensibo", configuration_url="https://home.sensibo.com/", diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 183c4db4b87..bcad658c700 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -86,7 +86,6 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def native_value(self) -> float | None: diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index f64411ff4dc..a8cfc527704 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -36,7 +36,7 @@ DEVICE_SELECT_TYPES = ( key="horizontalSwing", remote_key="horizontal_swing_mode", remote_options="horizontal_swing_modes", - name="Horizontal Swing", + name="Horizontal swing", icon="mdi:air-conditioner", ), SensiboSelectEntityDescription( @@ -79,7 +79,6 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def current_option(self) -> str | None: diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 8a53a0febd4..7a17db85a5b 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -69,7 +69,7 @@ class SensiboDeviceSensorEntityDescription( FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription( key="filter_last_reset", device_class=SensorDeviceClass.TIMESTAMP, - name="Filter Last Reset", + name="Filter last reset", icon="mdi:timer", value_fn=lambda data: data.filter_last_reset, extra_fn=None, @@ -93,7 +93,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=SensorStateClass.MEASUREMENT, - name="Battery Voltage", + name="Battery voltage", icon="mdi:battery", value_fn=lambda data: data.battery_voltage, ), @@ -128,7 +128,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( ), SensiboDeviceSensorEntityDescription( key="pure_sensitivity", - name="Pure Sensitivity", + name="Pure sensitivity", icon="mdi:air-filter", value_fn=lambda data: data.pure_sensitivity, extra_fn=None, @@ -140,7 +140,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="timer_time", device_class=SensorDeviceClass.TIMESTAMP, - name="Timer End Time", + name="Timer end time", icon="mdi:timer", value_fn=lambda data: data.timer_time, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, @@ -149,7 +149,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( key="feels_like", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - name="Temperature Feels Like", + name="Temperature feels like", value_fn=lambda data: data.feelslike, extra_fn=None, entity_registry_enabled_default=False, @@ -237,13 +237,10 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): device_id, sensor_id, sensor_data, - entity_description.name, ) self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" - self._attr_name = ( - f"{self.device_data.name} Motion Sensor {entity_description.name}" - ) + self._attr_name = entity_description.name @property def native_unit_of_measurement(self) -> str | None: @@ -280,7 +277,6 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def native_unit_of_measurement(self) -> str | None: diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index d9cf9417504..14cfaac73ae 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -135,7 +135,6 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/sensibo/update.py b/homeassistant/components/sensibo/update.py index 48304cbd3c5..67e0cbd6e65 100644 --- a/homeassistant/components/sensibo/update.py +++ b/homeassistant/components/sensibo/update.py @@ -43,7 +43,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceUpdateEntityDescription, ...] = ( key="fw_ver_available", device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.DIAGNOSTIC, - name="Update Available", + name="Update available", icon="mdi:rocket-launch", value_version=lambda data: data.fw_ver, value_available=lambda data: data.fw_ver_available, @@ -81,7 +81,6 @@ class SensiboDeviceUpdate(SensiboDeviceBaseEntity, UpdateEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" self._attr_title = self.device_data.model @property From d2e5d01acad3d60b2bb057dd65b50118f6cd1831 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Jul 2022 10:52:07 +0200 Subject: [PATCH 476/820] Migrate Yale Smart Alarm to new entity naming style (#75202) --- .../components/yale_smart_alarm/alarm_control_panel.py | 2 -- .../components/yale_smart_alarm/binary_sensor.py | 6 +----- homeassistant/components/yale_smart_alarm/button.py | 8 +++++--- homeassistant/components/yale_smart_alarm/entity.py | 7 +++++-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index fbd3f945aa2..e2df1b09ebe 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -14,7 +14,6 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -47,7 +46,6 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: """Initialize the Yale Alarm Device.""" super().__init__(coordinator) - self._attr_name = coordinator.entry.data[CONF_NAME] self._attr_unique_id = coordinator.entry.entry_id async def async_alarm_disarm(self, code: str | None = None) -> None: diff --git a/homeassistant/components/yale_smart_alarm/binary_sensor.py b/homeassistant/components/yale_smart_alarm/binary_sensor.py index 566dbed8c33..635fb3fad60 100644 --- a/homeassistant/components/yale_smart_alarm/binary_sensor.py +++ b/homeassistant/components/yale_smart_alarm/binary_sensor.py @@ -7,7 +7,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -21,7 +20,7 @@ SENSOR_TYPES = ( key="acfail", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, - name="Power Loss", + name="Power loss", ), BinarySensorEntityDescription( key="battery", @@ -85,9 +84,6 @@ class YaleProblemSensor(YaleAlarmEntity, BinarySensorEntity): """Initiate Yale Problem Sensor.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = ( - f"{coordinator.entry.data[CONF_NAME]} {entity_description.name}" - ) self._attr_unique_id = f"{coordinator.entry.entry_id}-{entity_description.key}" @property diff --git a/homeassistant/components/yale_smart_alarm/button.py b/homeassistant/components/yale_smart_alarm/button.py index cd312e79ceb..d8601ec85f9 100644 --- a/homeassistant/components/yale_smart_alarm/button.py +++ b/homeassistant/components/yale_smart_alarm/button.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,7 +13,11 @@ from .coordinator import YaleDataUpdateCoordinator from .entity import YaleAlarmEntity BUTTON_TYPES = ( - ButtonEntityDescription(key="panic", name="Panic Button", icon="mdi:alarm-light"), + ButtonEntityDescription( + key="panic", + name="Panic button", + icon="mdi:alarm-light", + ), ) @@ -47,7 +50,6 @@ class YalePanicButton(YaleAlarmEntity, ButtonEntity): """Initialize the plug switch.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.entry.data[CONF_NAME]} {description.name}" self._attr_unique_id = f"yale_smart_alarm-{description.key}" async def async_press(self) -> None: diff --git a/homeassistant/components/yale_smart_alarm/entity.py b/homeassistant/components/yale_smart_alarm/entity.py index b9a832f88e8..86b5839b51f 100644 --- a/homeassistant/components/yale_smart_alarm/entity.py +++ b/homeassistant/components/yale_smart_alarm/entity.py @@ -12,13 +12,14 @@ from .coordinator import YaleDataUpdateCoordinator class YaleEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): """Base implementation for Yale device.""" + _attr_has_entity_name = True + def __init__(self, coordinator: YaleDataUpdateCoordinator, data: dict) -> None: """Initialize an Yale device.""" super().__init__(coordinator) - self._attr_name: str = data["name"] self._attr_unique_id: str = data["address"] self._attr_device_info: DeviceInfo = DeviceInfo( - name=self._attr_name, + name=data["name"], manufacturer=MANUFACTURER, model=MODEL, identifiers={(DOMAIN, data["address"])}, @@ -29,6 +30,8 @@ class YaleEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): class YaleAlarmEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): """Base implementation for Yale Alarm device.""" + _attr_has_entity_name = True + def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: """Initialize an Yale device.""" super().__init__(coordinator) From 3f3ed3a2c5927a5b1230f1333eac80cec2437a06 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Jul 2022 10:52:40 +0200 Subject: [PATCH 477/820] Add multi-factor authentication support to Verisure (#75113) Co-authored-by: Paulus Schoutsen --- homeassistant/components/verisure/__init__.py | 14 +- .../components/verisure/config_flow.py | 142 +++++++- .../components/verisure/coordinator.py | 4 +- .../components/verisure/manifest.json | 2 +- .../components/verisure/strings.json | 15 +- .../components/verisure/translations/en.json | 15 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/verisure/test_config_flow.py | 312 +++++++++++++++++- 9 files changed, 479 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 9a64061554d..9ad8db08d59 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -3,9 +3,10 @@ from __future__ import annotations from contextlib import suppress import os +from pathlib import Path from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.const import CONF_EMAIL, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv @@ -28,6 +29,8 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Verisure from a config entry.""" + await hass.async_add_executor_job(migrate_cookie_files, hass, entry) + coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) if not await coordinator.async_login(): @@ -64,3 +67,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN] return True + + +def migrate_cookie_files(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Migrate old cookie file to new location.""" + cookie_file = Path(hass.config.path(STORAGE_DIR, f"verisure_{entry.unique_id}")) + if cookie_file.exists(): + cookie_file.rename( + hass.config.path(STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}") + ) diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 119a9250736..d53c7c9ed66 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -13,9 +13,10 @@ from verisure import ( import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.storage import STORAGE_DIR from .const import ( CONF_GIID, @@ -53,13 +54,34 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): self.email = user_input[CONF_EMAIL] self.password = user_input[CONF_PASSWORD] self.verisure = Verisure( - username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + username=self.email, + password=self.password, + cookieFileName=self.hass.config.path( + STORAGE_DIR, f"verisure_{user_input[CONF_EMAIL]}" + ), ) + try: await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: - LOGGER.debug("Could not log in to Verisure, %s", ex) - errors["base"] = "invalid_auth" + if "Multifactor authentication enabled" in str(ex): + try: + await self.hass.async_add_executor_job(self.verisure.login_mfa) + except ( + VerisureLoginError, + VerisureError, + VerisureResponseError, + ) as mfa_ex: + LOGGER.debug( + "Unexpected response from Verisure during MFA set up, %s", + mfa_ex, + ) + errors["base"] = "unknown_mfa" + else: + return await self.async_step_mfa() + else: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" except (VerisureError, VerisureResponseError) as ex: LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" @@ -77,6 +99,39 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle multifactor authentication step.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + self.verisure.mfa_validate, user_input[CONF_CODE], True + ) + await self.hass.async_add_executor_job(self.verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + return await self.async_step_installation() + + return self.async_show_form( + step_id="mfa", + data_schema=vol.Schema( + { + vol.Required(CONF_CODE): vol.All( + vol.Coerce(str), vol.Length(min=6, max=6) + ) + } + ), + errors=errors, + ) + async def async_step_installation( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -123,14 +178,38 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - verisure = Verisure( - username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + self.email = user_input[CONF_EMAIL] + self.password = user_input[CONF_PASSWORD] + + self.verisure = Verisure( + username=self.email, + password=self.password, + cookieFileName=self.hass.config.path( + STORAGE_DIR, f"verisure-{user_input[CONF_EMAIL]}" + ), ) + try: - await self.hass.async_add_executor_job(verisure.login) + await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: - LOGGER.debug("Could not log in to Verisure, %s", ex) - errors["base"] = "invalid_auth" + if "Multifactor authentication enabled" in str(ex): + try: + await self.hass.async_add_executor_job(self.verisure.login_mfa) + except ( + VerisureLoginError, + VerisureError, + VerisureResponseError, + ) as mfa_ex: + LOGGER.debug( + "Unexpected response from Verisure during MFA set up, %s", + mfa_ex, + ) + errors["base"] = "unknown_mfa" + else: + return await self.async_step_reauth_mfa() + else: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" except (VerisureError, VerisureResponseError) as ex: LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" @@ -160,6 +239,51 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle multifactor authentication step during re-authentication.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + self.verisure.mfa_validate, user_input[CONF_CODE], True + ) + await self.hass.async_add_executor_job(self.verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_EMAIL: self.email, + CONF_PASSWORD: self.password, + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_mfa", + data_schema=vol.Schema( + { + vol.Required(CONF_CODE): vol.All( + vol.Coerce(str), + vol.Length(min=6, max=6), + ) + } + ), + errors=errors, + ) + class VerisureOptionsFlowHandler(OptionsFlow): """Handle Verisure options.""" diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index 821e2830339..17cadb9598f 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -31,7 +31,9 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): self.verisure = Verisure( username=entry.data[CONF_EMAIL], password=entry.data[CONF_PASSWORD], - cookieFileName=hass.config.path(STORAGE_DIR, f"verisure_{entry.entry_id}"), + cookieFileName=hass.config.path( + STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}" + ), ) super().__init__( diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index c71be7ee4fc..820b8a20f14 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -2,7 +2,7 @@ "domain": "verisure", "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", - "requirements": ["vsure==1.7.3"], + "requirements": ["vsure==1.8.1"], "codeowners": ["@frenck"], "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json index 5170bff5faa..c8326d73756 100644 --- a/homeassistant/components/verisure/strings.json +++ b/homeassistant/components/verisure/strings.json @@ -8,6 +8,12 @@ "password": "[%key:common::config_flow::data::password%]" } }, + "mfa": { + "data": { + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", + "code": "Verification Code" + } + }, "installation": { "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant.", "data": { @@ -20,11 +26,18 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_mfa": { + "data": { + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", + "code": "Verification Code" + } } }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "unknown_mfa": "Unknown error occurred during MFA set up" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/verisure/translations/en.json b/homeassistant/components/verisure/translations/en.json index 57f73c3772b..34193ce0d09 100644 --- a/homeassistant/components/verisure/translations/en.json +++ b/homeassistant/components/verisure/translations/en.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unknown_mfa": "Unknown error occurred during MFA set up" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant." }, + "mfa": { + "data": { + "code": "Verification Code", + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you." + } + }, "reauth_confirm": { "data": { "description": "Re-authenticate with your Verisure My Pages account.", @@ -22,6 +29,12 @@ "password": "Password" } }, + "reauth_mfa": { + "data": { + "code": "Verification Code", + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you." + } + }, "user": { "data": { "description": "Sign-in with your Verisure My Pages account.", diff --git a/requirements_all.txt b/requirements_all.txt index cf89a5622bd..0ef7f82524e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2402,7 +2402,7 @@ volkszaehler==0.3.2 volvooncall==0.10.0 # homeassistant.components.verisure -vsure==1.7.3 +vsure==1.8.1 # homeassistant.components.vasttrafik vtjp==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 009ccb04343..926e89c8f2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1602,7 +1602,7 @@ venstarcolortouch==0.17 vilfo-api-client==0.3.2 # homeassistant.components.verisure -vsure==1.7.3 +vsure==1.8.1 # homeassistant.components.vulcan vulcan-api==2.1.1 diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index d957709c878..43adc91c38c 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -106,6 +106,129 @@ async def test_full_user_flow_multiple_installations( assert len(mock_setup_entry.mock_calls) == 1 +async def test_full_user_flow_single_installation_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: + """Test a full user initiated flow with a single installation and mfa.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "ascending (12345th street)" + assert result3.get("data") == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_user_flow_multiple_installations_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: + """Test a full user initiated configuration flow with a single installation.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("step_id") == "installation" + assert result3.get("type") == FlowResultType.FORM + assert result3.get("errors") is None + assert "flow_id" in result2 + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], {"giid": "54321"} + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("title") == "descending (54321th street)" + assert result4.get("data") == { + CONF_GIID: "54321", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + @pytest.mark.parametrize( "side_effect,error", [ @@ -142,10 +265,10 @@ async def test_verisure_errors( assert result2.get("errors") == {"base": error} assert "flow_id" in result2 - mock_verisure_config_flow.login.side_effect = None - mock_verisure_config_flow.installations = [ - mock_verisure_config_flow.installations[0] - ] + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + mock_verisure_config_flow.login_mfa.side_effect = side_effect result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -156,15 +279,65 @@ async def test_verisure_errors( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.CREATE_ENTRY - assert result3.get("title") == "ascending (12345th street)" - assert result3.get("data") == { + mock_verisure_config_flow.login_mfa.side_effect = None + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "user" + assert result3.get("errors") == {"base": "unknown_mfa"} + assert "flow_id" in result3 + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.FORM + assert result4.get("step_id") == "mfa" + assert "flow_id" in result4 + + mock_verisure_config_flow.mfa_validate.side_effect = side_effect + + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + "code": "123456", + }, + ) + assert result5.get("type") == FlowResultType.FORM + assert result5.get("step_id") == "mfa" + assert result5.get("errors") == {"base": error} + assert "flow_id" in result5 + + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + mock_verisure_config_flow.mfa_validate.side_effect = None + mock_verisure_config_flow.login.side_effect = None + + result6 = await hass.config_entries.flow.async_configure( + result5["flow_id"], + { + "code": "654321", + }, + ) + await hass.async_block_till_done() + + assert result6.get("type") == FlowResultType.CREATE_ENTRY + assert result6.get("title") == "ascending (12345th street)" + assert result6.get("data") == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 4 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 assert len(mock_setup_entry.mock_calls) == 1 @@ -226,6 +399,70 @@ async def test_reauth_flow( assert len(mock_setup_entry.mock_calls) == 1 +async def test_reauth_flow_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a reauthentication flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result.get("step_id") == "reauth_confirm" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "reauth_mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.ABORT + assert result3.get("reason") == "reauth_successful" + assert mock_config_entry.data == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "correct horse battery staple!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + @pytest.mark.parametrize( "side_effect,error", [ @@ -271,16 +508,63 @@ async def test_reauth_flow_errors( assert result2.get("errors") == {"base": error} assert "flow_id" in result2 + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + mock_verisure_config_flow.login_mfa.side_effect = side_effect + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "reauth_confirm" + assert result3.get("errors") == {"base": "unknown_mfa"} + assert "flow_id" in result3 + + mock_verisure_config_flow.login_mfa.side_effect = None + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.FORM + assert result4.get("step_id") == "reauth_mfa" + assert "flow_id" in result4 + + mock_verisure_config_flow.mfa_validate.side_effect = side_effect + + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + "code": "123456", + }, + ) + assert result5.get("type") == FlowResultType.FORM + assert result5.get("step_id") == "reauth_mfa" + assert result5.get("errors") == {"base": error} + assert "flow_id" in result5 + + mock_verisure_config_flow.mfa_validate.side_effect = None mock_verisure_config_flow.login.side_effect = None mock_verisure_config_flow.installations = [ mock_verisure_config_flow.installations[0] ] await hass.config_entries.flow.async_configure( - result2["flow_id"], + result5["flow_id"], { - "email": "verisure_my_pages@example.com", - "password": "correct horse battery staple", + "code": "654321", }, ) await hass.async_block_till_done() @@ -288,10 +572,12 @@ async def test_reauth_flow_errors( assert mock_config_entry.data == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", - CONF_PASSWORD: "correct horse battery staple", + CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 4 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 assert len(mock_setup_entry.mock_calls) == 1 From 06e905054eefa2de3fc49bb8b44dbed2fe9eb598 Mon Sep 17 00:00:00 2001 From: apaperclip <67401560+apaperclip@users.noreply.github.com> Date: Fri, 15 Jul 2022 04:57:23 -0400 Subject: [PATCH 478/820] Fix aruba ssh host key algorithm (#75224) --- homeassistant/components/aruba/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index ecdcc5f70f2..dc2d2fee8e9 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -87,7 +87,7 @@ class ArubaDeviceScanner(DeviceScanner): def get_aruba_data(self): """Retrieve data from Aruba Access Point and return parsed result.""" - connect = f"ssh {self.username}@{self.host}" + connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa" ssh = pexpect.spawn(connect) query = ssh.expect( [ From 48f4b51a1d985b7429bd812991104bcb27f89828 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Jul 2022 11:07:39 +0200 Subject: [PATCH 479/820] Migrate DNSIP to new entity naming style (#75197) --- homeassistant/components/dnsip/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index a770afe388d..93bf73f1b9d 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -52,6 +52,7 @@ class WanIpSensor(SensorEntity): """Implementation of a DNS IP sensor.""" _attr_icon = "mdi:web" + _attr_has_entity_name = True def __init__( self, @@ -61,7 +62,7 @@ class WanIpSensor(SensorEntity): ipv6: bool, ) -> None: """Initialize the DNS IP sensor.""" - self._attr_name = f"{name} IPv6" if ipv6 else name + self._attr_name = "IPv6" if ipv6 else None self._attr_unique_id = f"{hostname}_{ipv6}" self.hostname = hostname self.resolver = aiodns.DNSResolver() @@ -76,7 +77,7 @@ class WanIpSensor(SensorEntity): identifiers={(DOMAIN, f"{hostname}_{ipv6}")}, manufacturer="DNS", model=aiodns.__version__, - name=hostname, + name=name, ) async def async_update(self) -> None: From c6c063e8c52612a524e387400e6f4a919ac77a57 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Jul 2022 12:38:20 +0200 Subject: [PATCH 480/820] Various cleanups in AdGuard Home (#75250) Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + homeassistant/components/adguard/__init__.py | 101 +------------------ homeassistant/components/adguard/const.py | 3 + homeassistant/components/adguard/entity.py | 69 +++++++++++++ homeassistant/components/adguard/sensor.py | 14 +-- homeassistant/components/adguard/switch.py | 27 ++--- 6 files changed, 86 insertions(+), 129 deletions(-) create mode 100644 homeassistant/components/adguard/entity.py diff --git a/.coveragerc b/.coveragerc index 94b9eceb11b..cfc51dc700d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -23,6 +23,7 @@ omit = homeassistant/components/adax/climate.py homeassistant/components/adguard/__init__.py homeassistant/components/adguard/const.py + homeassistant/components/adguard/entity.py homeassistant/components/adguard/sensor.py homeassistant/components/adguard/switch.py homeassistant/components/ads/* diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index e27aef6389d..fbcbea61316 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,12 +1,10 @@ """Support for AdGuard Home.""" from __future__ import annotations -import logging - -from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError +from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol -from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -22,13 +20,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( CONF_FORCE, DATA_ADGUARD_CLIENT, - DATA_ADGUARD_VERSION, DOMAIN, SERVICE_ADD_URL, SERVICE_DISABLE_URL, @@ -37,8 +32,6 @@ from .const import ( SERVICE_REMOVE_URL, ) -_LOGGER = logging.getLogger(__name__) - SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url}) SERVICE_ADD_URL_SCHEMA = vol.Schema( {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.url} @@ -127,93 +120,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN] return unload_ok - - -class AdGuardHomeEntity(Entity): - """Defines a base AdGuard Home entity.""" - - _attr_has_entity_name = True - - def __init__( - self, - adguard: AdGuardHome, - entry: ConfigEntry, - name: str | None, - icon: str | None, - enabled_default: bool = True, - ) -> None: - """Initialize the AdGuard Home entity.""" - self._available = True - self._enabled_default = enabled_default - self._icon = icon - self._name = name - self._entry = entry - self.adguard = adguard - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str | None: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - async def async_update(self) -> None: - """Update AdGuard Home entity.""" - if not self.enabled: - return - - try: - await self._adguard_update() - self._available = True - except AdGuardHomeError: - if self._available: - _LOGGER.debug( - "An error occurred while updating AdGuard Home sensor", - exc_info=True, - ) - self._available = False - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - raise NotImplementedError() - - -class AdGuardHomeDeviceEntity(AdGuardHomeEntity): - """Defines a AdGuard Home device entity.""" - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this AdGuard Home instance.""" - if self._entry.source == SOURCE_HASSIO: - config_url = "homeassistant://hassio/ingress/a0d7b954_adguard" - else: - if self.adguard.tls: - config_url = f"https://{self.adguard.host}:{self.adguard.port}" - else: - config_url = f"http://{self.adguard.host}:{self.adguard.port}" - - return DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={ - (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] - }, - manufacturer="AdGuard Team", - name="AdGuard Home", - sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( - DATA_ADGUARD_VERSION - ), - configuration_url=config_url, - ) diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index 8bfa5b49fc6..a4ccde68539 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -1,7 +1,10 @@ """Constants for the AdGuard Home integration.""" +import logging DOMAIN = "adguard" +LOGGER = logging.getLogger(__package__) + DATA_ADGUARD_CLIENT = "adguard_client" DATA_ADGUARD_VERSION = "adguard_version" diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py new file mode 100644 index 00000000000..7d6bf099366 --- /dev/null +++ b/homeassistant/components/adguard/entity.py @@ -0,0 +1,69 @@ +"""AdGuard Home base entity.""" +from __future__ import annotations + +from adguardhome import AdGuardHome, AdGuardHomeError + +from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER + + +class AdGuardHomeEntity(Entity): + """Defines a base AdGuard Home entity.""" + + _attr_has_entity_name = True + _attr_available = True + + def __init__( + self, + adguard: AdGuardHome, + entry: ConfigEntry, + ) -> None: + """Initialize the AdGuard Home entity.""" + self._entry = entry + self.adguard = adguard + + async def async_update(self) -> None: + """Update AdGuard Home entity.""" + if not self.enabled: + return + + try: + await self._adguard_update() + self._attr_available = True + except AdGuardHomeError: + if self._attr_available: + LOGGER.debug( + "An error occurred while updating AdGuard Home sensor", + exc_info=True, + ) + self._attr_available = False + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + raise NotImplementedError() + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this AdGuard Home instance.""" + if self._entry.source == SOURCE_HASSIO: + config_url = "homeassistant://hassio/ingress/a0d7b954_adguard" + elif self.adguard.tls: + config_url = f"https://{self.adguard.host}:{self.adguard.port}" + else: + config_url = f"http://{self.adguard.host}:{self.adguard.port}" + + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={ + (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] + }, + manufacturer="AdGuard Team", + name="AdGuard Home", + sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( + DATA_ADGUARD_VERSION + ), + configuration_url=config_url, + ) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 19ed0b96801..07a483f03c4 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -15,8 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AdGuardHomeDeviceEntity from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN +from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=300) PARALLEL_UPDATES = 4 @@ -118,7 +118,7 @@ async def async_setup_entry( ) -class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): +class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity): """Defines a AdGuard Home sensor.""" entity_description: AdGuardHomeEntityDescription @@ -130,8 +130,8 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): description: AdGuardHomeEntityDescription, ) -> None: """Initialize AdGuard Home sensor.""" + super().__init__(adguard, entry) self.entity_description = description - self._attr_unique_id = "_".join( [ DOMAIN, @@ -142,14 +142,6 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): ] ) - super().__init__( - adguard, - entry, - description.name, - description.icon, - description.entity_registry_enabled_default, - ) - async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" value = await self.entity_description.value_fn(self.adguard)() diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index efbd5c7fa38..a359bf86c2d 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import timedelta -import logging from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError @@ -15,10 +14,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AdGuardHomeDeviceEntity -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN, LOGGER +from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 1 @@ -113,7 +110,7 @@ async def async_setup_entry( ) -class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): +class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity): """Defines a AdGuard Home switch.""" entity_description: AdGuardHomeSwitchEntityDescription @@ -125,35 +122,27 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): description: AdGuardHomeSwitchEntityDescription, ) -> None: """Initialize AdGuard Home switch.""" + super().__init__(adguard, entry) self.entity_description = description - self._attr_unique_id = "_".join( [DOMAIN, adguard.host, str(adguard.port), "switch", description.key] ) - super().__init__( - adguard, - entry, - description.name, - description.icon, - description.entity_registry_enabled_default, - ) - async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" try: await self.entity_description.turn_off_fn(self.adguard)() except AdGuardHomeError: - _LOGGER.error("An error occurred while turning off AdGuard Home switch") - self._available = False + LOGGER.error("An error occurred while turning off AdGuard Home switch") + self._attr_available = False async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" try: await self.entity_description.turn_on_fn(self.adguard)() except AdGuardHomeError: - _LOGGER.error("An error occurred while turning on AdGuard Home switch") - self._available = False + LOGGER.error("An error occurred while turning on AdGuard Home switch") + self._attr_available = False async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" From 97fd6699249115ee23b4ad4cadd9e1ec8d95ac49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Jul 2022 17:44:25 +0200 Subject: [PATCH 481/820] HomeKit Controller BLE Fixes (#75271) --- homeassistant/components/homekit_controller/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index ac840eb0689..41094a65e00 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -6,6 +6,7 @@ import re from typing import Any import aiohomekit +from aiohomekit.controller.abstract import AbstractPairing from aiohomekit.exceptions import AuthenticationError from aiohomekit.model import Accessories, CharacteristicsTypes, ServicesTypes from aiohomekit.utils import domain_supported, domain_to_name @@ -468,13 +469,13 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema(schema), ) - async def _entry_from_accessory(self, pairing): + async def _entry_from_accessory(self, pairing: AbstractPairing) -> FlowResult: """Return a config entry from an initialized bridge.""" # The bulk of the pairing record is stored on the config entry. # A specific exception is the 'accessories' key. This is more # volatile. We do cache it, but not against the config entry. # So copy the pairing data and mutate the copy. - pairing_data = pairing.pairing_data.copy() + pairing_data = pairing.pairing_data.copy() # type: ignore[attr-defined] # Use the accessories data from the pairing operation if it is # available. Otherwise request a fresh copy from the API. @@ -489,6 +490,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) name = accessory_info.value(CharacteristicsTypes.NAME, "") + await pairing.close() + return self.async_create_entry(title=name, data=pairing_data) From 2106c9f24723c422fd2d95b5d17e78c707e74e27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Jul 2022 18:21:09 +0200 Subject: [PATCH 482/820] Fix delay adding entities in HKC (#75273) --- .../components/homekit_controller/__init__.py | 4 +++- .../components/homekit_controller/connection.py | 4 ++-- .../components/homekit_controller/device_trigger.py | 12 +++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 461d46eed1d..9175dca50bc 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -76,7 +76,9 @@ class HomeKitEntity(Entity): ) self._accessory.add_pollable_characteristics(self.pollable_characteristics) - self._accessory.add_watchable_characteristics(self.watchable_characteristics) + await self._accessory.add_watchable_characteristics( + self.watchable_characteristics + ) async def async_will_remove_from_hass(self) -> None: """Prepare to be removed from hass.""" diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index e1cbedb01e8..85e8e3987bd 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -145,12 +145,12 @@ class HKDevice: char for char in self.pollable_characteristics if char[0] != accessory_id ] - def add_watchable_characteristics( + async def add_watchable_characteristics( self, characteristics: list[tuple[int, int]] ) -> None: """Add (aid, iid) pairs that we need to poll.""" self.watchable_characteristics.extend(characteristics) - self.hass.async_create_task(self.pairing.subscribe(characteristics)) + await self.pairing.subscribe(characteristics) def remove_watchable_characteristics(self, accessory_id: int) -> None: """Remove all pollable characteristics by accessory id.""" diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index dcac7238c8e..49924b30c57 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for homekit devices.""" from __future__ import annotations -from collections.abc import Generator +from collections.abc import Callable, Generator from typing import TYPE_CHECKING, Any from aiohomekit.model.characteristics import CharacteristicsTypes @@ -59,7 +59,9 @@ HK_TO_HA_INPUT_EVENT_VALUES = { class TriggerSource: """Represents a stateless source of event data from HomeKit.""" - def __init__(self, connection, aid, triggers): + def __init__( + self, connection: HKDevice, aid: int, triggers: list[dict[str, Any]] + ) -> None: """Initialize a set of triggers for a device.""" self._hass = connection.hass self._connection = connection @@ -67,7 +69,7 @@ class TriggerSource: self._triggers: dict[tuple[str, str], dict[str, Any]] = {} for trigger in triggers: self._triggers[(trigger["type"], trigger["subtype"])] = trigger - self._callbacks = {} + self._callbacks: dict[int, list[Callable[[Any], None]]] = {} def fire(self, iid, value): """Process events that have been received from a HomeKit accessory.""" @@ -97,7 +99,7 @@ class TriggerSource: trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] iid = trigger["characteristic"] - self._connection.add_watchable_characteristics([(self._aid, iid)]) + await self._connection.add_watchable_characteristics([(self._aid, iid)]) self._callbacks.setdefault(iid, []).append(event_handler) def async_remove_handler(): @@ -196,7 +198,7 @@ TRIGGER_FINDERS = { async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): """Triggers aren't entities as they have no state, but we still need to set them up for a config entry.""" hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service): From dbcd98d0294750c7ef946d7df0379ecd3f204caf Mon Sep 17 00:00:00 2001 From: Khole Date: Fri, 15 Jul 2022 22:03:22 +0100 Subject: [PATCH 483/820] Add fixes for hive light (#75286) --- homeassistant/components/hive/light.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index c06237f3709..69345c430c7 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -44,13 +44,15 @@ class HiveDeviceLight(HiveEntity, LightEntity): super().__init__(hive, hive_device) if self.device["hiveType"] == "warmwhitelight": self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + self._attr_color_mode = ColorMode.BRIGHTNESS elif self.device["hiveType"] == "tuneablelight": self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} + self._attr_color_mode = ColorMode.COLOR_TEMP elif self.device["hiveType"] == "colourtuneablelight": self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} - self._attr_min_mireds = self.device.get("min_mireds") - self._attr_max_mireds = self.device.get("max_mireds") + self._attr_min_mireds = 153 + self._attr_max_mireds = 370 @refresh_system async def async_turn_on(self, **kwargs): @@ -94,6 +96,13 @@ class HiveDeviceLight(HiveEntity, LightEntity): if self._attr_available: self._attr_is_on = self.device["status"]["state"] self._attr_brightness = self.device["status"]["brightness"] + if self.device["hiveType"] == "tuneablelight": + self._attr_color_temp = self.device["status"].get("color_temp") if self.device["hiveType"] == "colourtuneablelight": - rgb = self.device["status"]["hs_color"] - self._attr_hs_color = color_util.color_RGB_to_hs(*rgb) + if self.device["status"]["mode"] == "COLOUR": + rgb = self.device["status"]["hs_color"] + self._attr_hs_color = color_util.color_RGB_to_hs(*rgb) + self._attr_color_mode = ColorMode.HS + else: + self._attr_color_temp = self.device["status"].get("color_temp") + self._attr_color_mode = ColorMode.COLOR_TEMP From 1ce47147226da8e6bc392a4113e2b3a6d46c2839 Mon Sep 17 00:00:00 2001 From: clayton craft Date: Fri, 15 Jul 2022 15:38:06 -0700 Subject: [PATCH 484/820] Bump venstarcolortouch to 0.18 (#75237) venstarcolortouch: bump to 0.18 --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 2f3331af6e2..4a6eea28e24 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.17"], + "requirements": ["venstarcolortouch==0.18"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index 0ef7f82524e..94759b214d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2390,7 +2390,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.17 +venstarcolortouch==0.18 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 926e89c8f2a..397cc15e05c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1596,7 +1596,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.17 +venstarcolortouch==0.18 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From d0c4d39ae20841b441b497cdd42e6b48f6162c2c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 16 Jul 2022 00:26:06 +0000 Subject: [PATCH 485/820] [ci skip] Translation update --- .../components/derivative/translations/ru.json | 4 ++-- .../here_travel_time/translations/nl.json | 6 ++++++ .../components/nina/translations/nl.json | 6 ++++++ .../components/rhasspy/translations/nl.json | 12 ++++++++++++ .../components/verisure/translations/ca.json | 15 ++++++++++++++- .../components/verisure/translations/de.json | 15 ++++++++++++++- .../components/verisure/translations/et.json | 15 ++++++++++++++- .../components/verisure/translations/fr.json | 15 ++++++++++++++- .../components/verisure/translations/ja.json | 15 ++++++++++++++- .../components/verisure/translations/nl.json | 10 ++++++++++ .../components/verisure/translations/pt-BR.json | 15 ++++++++++++++- .../components/verisure/translations/zh-Hant.json | 15 ++++++++++++++- .../components/withings/translations/nl.json | 3 +++ 13 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/rhasspy/translations/nl.json diff --git a/homeassistant/components/derivative/translations/ru.json b/homeassistant/components/derivative/translations/ru.json index 6155d64301a..bef5b20efdd 100644 --- a/homeassistant/components/derivative/translations/ru.json +++ b/homeassistant/components/derivative/translations/ru.json @@ -12,7 +12,7 @@ }, "data_description": { "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", - "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", + "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439." }, "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0443\u044e \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", @@ -33,7 +33,7 @@ }, "data_description": { "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", - "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", + "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439.." } } diff --git a/homeassistant/components/here_travel_time/translations/nl.json b/homeassistant/components/here_travel_time/translations/nl.json index cbec21776e5..7d3438901cc 100644 --- a/homeassistant/components/here_travel_time/translations/nl.json +++ b/homeassistant/components/here_travel_time/translations/nl.json @@ -39,6 +39,12 @@ }, "title": "Herkomst kiezen" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Een kaartlocatie gebruiken", + "origin_entity": "Een entiteit gebruiken" + } + }, "user": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/nina/translations/nl.json b/homeassistant/components/nina/translations/nl.json index 1b407576bbd..7c18eb1e6b6 100644 --- a/homeassistant/components/nina/translations/nl.json +++ b/homeassistant/components/nina/translations/nl.json @@ -31,6 +31,12 @@ }, "step": { "init": { + "data": { + "_i_to_l": "Stad/provincie (I-L)", + "_m_to_q": "Stad/provincie (M-Q)", + "_r_to_u": "Stad/provincie (R-U)", + "_v_to_z": "Stad/provincie (V-Z)" + }, "title": "Opties" } } diff --git a/homeassistant/components/rhasspy/translations/nl.json b/homeassistant/components/rhasspy/translations/nl.json new file mode 100644 index 00000000000..7089a156d1b --- /dev/null +++ b/homeassistant/components/rhasspy/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wilt u Rhasspy-ondersteuning inschakelen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/ca.json b/homeassistant/components/verisure/translations/ca.json index 87e441fd937..36c76a15dcd 100644 --- a/homeassistant/components/verisure/translations/ca.json +++ b/homeassistant/components/verisure/translations/ca.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "unknown_mfa": "S'ha produ\u00eft un error desconegut durant la configuraci\u00f3 de l'MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant ha trobat diverses instal\u00b7lacions Verisure al compte de My Pages. Selecciona la instal\u00b7laci\u00f3 a afegir a Home Assistant." }, + "mfa": { + "data": { + "code": "Codi de verificaci\u00f3", + "description": "El teu compte t\u00e9 activada la verificaci\u00f3 en dos passos. Introdueix el codi de verificaci\u00f3 que t'ha enviat Verisure." + } + }, "reauth_confirm": { "data": { "description": "Torna a autenticar-te amb el compte de Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Contrasenya" } }, + "reauth_mfa": { + "data": { + "code": "Codi de verificaci\u00f3", + "description": "El teu compte t\u00e9 activada la verificaci\u00f3 en dos passos. Introdueix el codi de verificaci\u00f3 que t'ha enviat Verisure." + } + }, "user": { "data": { "description": "Inicia sessi\u00f3 amb el compte de Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/de.json b/homeassistant/components/verisure/translations/de.json index 3eaf6ff04f6..b5e544e579b 100644 --- a/homeassistant/components/verisure/translations/de.json +++ b/homeassistant/components/verisure/translations/de.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "unknown_mfa": "Beim MFA-Setup ist ein unbekannter Fehler aufgetreten" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant hat mehrere Verisure-Installationen in deinen My Pages-Konto gefunden. Bitte w\u00e4hle die Installation aus, die du zu Home Assistant hinzuf\u00fcgen m\u00f6chtest." }, + "mfa": { + "data": { + "code": "Verifizierungs-Code", + "description": "Dein Konto hat die 2-Schritt-Verifizierung aktiviert. Bitte gib den Verifizierungscode ein, den du von Verisure erhalten hast." + } + }, "reauth_confirm": { "data": { "description": "Authentifiziere dich erneut mit deinem Verisure My Pages-Konto.", @@ -22,6 +29,12 @@ "password": "Passwort" } }, + "reauth_mfa": { + "data": { + "code": "Verifizierungs-Code", + "description": "Dein Konto hat die 2-Schritt-Verifizierung aktiviert. Bitte gib den Verifizierungscode ein, den du von Verisure erhalten hast." + } + }, "user": { "data": { "description": "Melde dich mit deinen Verisure My Pages-Konto an.", diff --git a/homeassistant/components/verisure/translations/et.json b/homeassistant/components/verisure/translations/et.json index 78a2c987ef2..25fb59a8aad 100644 --- a/homeassistant/components/verisure/translations/et.json +++ b/homeassistant/components/verisure/translations/et.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Vigane autentimine", - "unknown": "Ootamatu t\u00f5rge" + "unknown": "Ootamatu t\u00f5rge", + "unknown_mfa": "MFA h\u00e4\u00e4lestamisel ilmnes tundmatu t\u00f5rge" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant leidis kontolt Minu lehed mitu Verisure paigaldust. Vali Home Assistantile lisatav paigaldus." }, + "mfa": { + "data": { + "code": "Kinnituskood", + "description": "Kontol on lubatud 2-astmeline kontroll. Palun sisesta Verisure'i poolt saadetud kinnituskood." + } + }, "reauth_confirm": { "data": { "description": "Taastuvasta oma Verisure My Pages'i kontoga.", @@ -22,6 +29,12 @@ "password": "Salas\u00f5na" } }, + "reauth_mfa": { + "data": { + "code": "Kinnituskood", + "description": "Kontol on lubatud 2-astmeline kontroll. Palun sisesta Verisure'i poolt saadetud kinnituskood." + } + }, "user": { "data": { "description": "Logi sisse oma Verisure My Pages kontoga.", diff --git a/homeassistant/components/verisure/translations/fr.json b/homeassistant/components/verisure/translations/fr.json index f3c26abb74a..0848eb6022b 100644 --- a/homeassistant/components/verisure/translations/fr.json +++ b/homeassistant/components/verisure/translations/fr.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Authentification non valide", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "unknown_mfa": "Une erreur inconnue est survenue lors de la configuration de l'authentification multifacteur" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant a trouv\u00e9 plusieurs installations Verisure dans votre compte My Pages. Veuillez s\u00e9lectionner l'installation \u00e0 ajouter \u00e0 Home Assistant." }, + "mfa": { + "data": { + "code": "Code de v\u00e9rification", + "description": "La v\u00e9rification en deux \u00e9tapes est activ\u00e9e sur votre compte. Veuillez saisir le code de v\u00e9rification envoy\u00e9 par Verisure." + } + }, "reauth_confirm": { "data": { "description": "R\u00e9-authentifiez-vous avec votre compte Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Mot de passe" } }, + "reauth_mfa": { + "data": { + "code": "Code de v\u00e9rification", + "description": "La v\u00e9rification en deux \u00e9tapes est activ\u00e9e sur votre compte. Veuillez saisir le code de v\u00e9rification envoy\u00e9 par Verisure." + } + }, "user": { "data": { "description": "Connectez-vous avec votre compte Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/ja.json b/homeassistant/components/verisure/translations/ja.json index 5bd85b67e34..9aa412a0214 100644 --- a/homeassistant/components/verisure/translations/ja.json +++ b/homeassistant/components/verisure/translations/ja.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unknown_mfa": "MFA\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant\u306f\u3001\u30de\u30a4\u30da\u30fc\u30b8\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u8907\u6570\u306eVerisure\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u691c\u51fa\u3057\u307e\u3057\u305f\u3002Home Assistant\u306b\u8ffd\u52a0\u3059\u308b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "mfa": { + "data": { + "code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9", + "description": "\u30a2\u30ab\u30a6\u30f3\u30c8\u30672\u6bb5\u968e\u8a8d\u8a3c\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u3059\u3002Verisure\u304b\u3089\u9001\u4fe1\u3055\u308c\u305f\u78ba\u8a8d\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + }, "reauth_confirm": { "data": { "description": "Verisure MyPages\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002", @@ -22,6 +29,12 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } }, + "reauth_mfa": { + "data": { + "code": "\u78ba\u8a8d\u30b3\u30fc\u30c9", + "description": "\u30a2\u30ab\u30a6\u30f3\u30c8\u30672\u6bb5\u968e\u8a8d\u8a3c\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u3059\u3002Verisure\u304b\u3089\u9001\u4fe1\u3055\u308c\u305f\u78ba\u8a8d\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + }, "user": { "data": { "description": "Verisure My Pages\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/verisure/translations/nl.json b/homeassistant/components/verisure/translations/nl.json index 1a23da57319..89dc4860e95 100644 --- a/homeassistant/components/verisure/translations/nl.json +++ b/homeassistant/components/verisure/translations/nl.json @@ -15,6 +15,11 @@ }, "description": "Home Assistant heeft meerdere Verisure-installaties gevonden in uw My Pages-account. Selecteer de installatie om toe te voegen aan Home Assistant." }, + "mfa": { + "data": { + "code": "Verificatiecode" + } + }, "reauth_confirm": { "data": { "description": "Verifieer opnieuw met uw Verisure My Pages-account.", @@ -22,6 +27,11 @@ "password": "Wachtwoord" } }, + "reauth_mfa": { + "data": { + "code": "Verificatiecode" + } + }, "user": { "data": { "description": "Aanmelden met Verisure My Pages-account.", diff --git a/homeassistant/components/verisure/translations/pt-BR.json b/homeassistant/components/verisure/translations/pt-BR.json index 1fc733bfd55..9d42afd0e30 100644 --- a/homeassistant/components/verisure/translations/pt-BR.json +++ b/homeassistant/components/verisure/translations/pt-BR.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unknown_mfa": "Ocorreu um erro desconhecido durante a configura\u00e7\u00e3o da MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "O Home Assistant encontrou v\u00e1rias instala\u00e7\u00f5es da Verisure na sua conta do My Pages. Por favor, selecione a instala\u00e7\u00e3o para adicionar ao Home Assistant." }, + "mfa": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o", + "description": "Sua conta tem a verifica\u00e7\u00e3o em duas etapas ativada. Insira o c\u00f3digo de verifica\u00e7\u00e3o que a Verisure envia para voc\u00ea." + } + }, "reauth_confirm": { "data": { "description": "Re-autentique com sua conta Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Senha" } }, + "reauth_mfa": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o", + "description": "Sua conta tem a verifica\u00e7\u00e3o em duas etapas ativada. Insira o c\u00f3digo de verifica\u00e7\u00e3o que a Verisure envia para voc\u00ea." + } + }, "user": { "data": { "description": "Fa\u00e7a login com sua conta Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/zh-Hant.json b/homeassistant/components/verisure/translations/zh-Hant.json index 29410e390fe..53eb4ab1eb7 100644 --- a/homeassistant/components/verisure/translations/zh-Hant.json +++ b/homeassistant/components/verisure/translations/zh-Hant.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unknown_mfa": "\u65bc\u591a\u6b65\u9a5f\u8a8d\u8b49\u8a2d\u5b9a\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant \u65bc My Pages \u5e33\u865f\u4e2d\u627e\u5230\u591a\u500b Verisure \u5b89\u88dd\u3002\u8acb\u9078\u64c7\u6240\u8981\u65b0\u589e\u81f3 Home Assistant \u7684\u9805\u76ee\u3002" }, + "mfa": { + "data": { + "code": "\u9a57\u8b49\u78bc", + "description": "\u5e33\u865f\u5177\u6709\u5169\u6b65\u9a5f\u9a57\u8b49\u3001\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684 Verisure \u9a57\u8b49\u78bc\u3002" + } + }, "reauth_confirm": { "data": { "description": "\u91cd\u65b0\u8a8d\u8b49 Verisure My Pages \u5e33\u865f\u3002", @@ -22,6 +29,12 @@ "password": "\u5bc6\u78bc" } }, + "reauth_mfa": { + "data": { + "code": "\u9a57\u8b49\u78bc", + "description": "\u5e33\u865f\u5177\u6709\u5169\u6b65\u9a5f\u9a57\u8b49\u3001\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684 Verisure \u9a57\u8b49\u78bc\u3002" + } + }, "user": { "data": { "description": "\u4ee5 Verisure My Pages \u5e33\u865f\u767b\u5165", diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index cca755effbb..8e40180b75e 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -27,6 +27,9 @@ "reauth": { "description": "Het {profile} \" moet opnieuw worden geverifieerd om Withings-gegevens te blijven ontvangen.", "title": "Integratie herauthenticeren" + }, + "reauth_confirm": { + "title": "Integratie herauthenticeren" } } } From 027cdbdb3893f3341629b7394e8bd0e24c6cd325 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 16 Jul 2022 14:33:18 +0200 Subject: [PATCH 486/820] Fix Sensibo new entity naming style (#75307) --- homeassistant/components/sensibo/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 7a17db85a5b..f21366c7aa6 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -240,7 +240,6 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" - self._attr_name = entity_description.name @property def native_unit_of_measurement(self) -> str | None: From 13cea26e7496ba671ad2bc56137fe59e8a463e20 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Jul 2022 15:21:19 +0200 Subject: [PATCH 487/820] Migrate Tailscale to new entity naming style (#75018) --- homeassistant/components/tailscale/__init__.py | 6 +++--- homeassistant/components/tailscale/binary_sensor.py | 2 +- homeassistant/components/tailscale/sensor.py | 4 ++-- tests/components/tailscale/test_binary_sensor.py | 2 +- tests/components/tailscale/test_sensor.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py index 2a5fca43ef1..abc4c4ca399 100644 --- a/homeassistant/components/tailscale/__init__.py +++ b/homeassistant/components/tailscale/__init__.py @@ -41,6 +41,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class TailscaleEntity(CoordinatorEntity): """Defines a Tailscale base entity.""" + _attr_has_entity_name = True + def __init__( self, *, @@ -52,8 +54,6 @@ class TailscaleEntity(CoordinatorEntity): super().__init__(coordinator=coordinator) self.entity_description = description self.device_id = device.device_id - self.friendly_name = device.name.split(".")[0] - self._attr_name = f"{self.friendly_name} {description.name}" self._attr_unique_id = f"{device.device_id}_{description.key}" @property @@ -71,6 +71,6 @@ class TailscaleEntity(CoordinatorEntity): identifiers={(DOMAIN, device.device_id)}, manufacturer="Tailscale Inc.", model=device.os, - name=self.friendly_name, + name=device.name.split(".")[0], sw_version=device.client_version, ) diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py index 2f97d307b15..94176916dec 100644 --- a/homeassistant/components/tailscale/binary_sensor.py +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -44,7 +44,7 @@ BINARY_SENSORS: tuple[TailscaleBinarySensorEntityDescription, ...] = ( ), TailscaleBinarySensorEntityDescription( key="client_supports_hair_pinning", - name="Supports Hairpinning", + name="Supports hairpinning", icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.hair_pinning, diff --git a/homeassistant/components/tailscale/sensor.py b/homeassistant/components/tailscale/sensor.py index 07f7dbe91cc..13d8a6db0cf 100644 --- a/homeassistant/components/tailscale/sensor.py +++ b/homeassistant/components/tailscale/sensor.py @@ -45,14 +45,14 @@ SENSORS: tuple[TailscaleSensorEntityDescription, ...] = ( ), TailscaleSensorEntityDescription( key="ip", - name="IP Address", + name="IP address", icon="mdi:ip-network", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.addresses[0] if device.addresses else None, ), TailscaleSensorEntityDescription( key="last_seen", - name="Last Seen", + name="Last seen", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.last_seen, ), diff --git a/tests/components/tailscale/test_binary_sensor.py b/tests/components/tailscale/test_binary_sensor.py index feb34c6d8a3..864fb497134 100644 --- a/tests/components/tailscale/test_binary_sensor.py +++ b/tests/components/tailscale/test_binary_sensor.py @@ -43,7 +43,7 @@ async def test_tailscale_binary_sensors( assert state.state == STATE_OFF assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "frencks-iphone Supports Hairpinning" + == "frencks-iphone Supports hairpinning" ) assert state.attributes.get(ATTR_ICON) == "mdi:wan" assert ATTR_DEVICE_CLASS not in state.attributes diff --git a/tests/components/tailscale/test_sensor.py b/tests/components/tailscale/test_sensor.py index 911de0eb64a..9f55bba2c70 100644 --- a/tests/components/tailscale/test_sensor.py +++ b/tests/components/tailscale/test_sensor.py @@ -35,7 +35,7 @@ async def test_tailscale_sensors( assert entry.unique_id == "123457_last_seen" assert entry.entity_category is None assert state.state == "2021-11-15T20:37:03+00:00" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Last Seen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Last seen" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes @@ -46,7 +46,7 @@ async def test_tailscale_sensors( assert entry.unique_id == "123457_ip" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "100.11.11.112" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router IP Address" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router IP address" assert state.attributes.get(ATTR_ICON) == "mdi:ip-network" assert ATTR_DEVICE_CLASS not in state.attributes From 0f3cc4a4aa2ac7965f5a563b86d03c8eeb6125d2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 16 Jul 2022 15:25:07 +0200 Subject: [PATCH 488/820] Migrate GIOS to new entity naming style (#75051) Use new entity naming style --- homeassistant/components/gios/const.py | 64 ----------------- homeassistant/components/gios/model.py | 14 ---- homeassistant/components/gios/sensor.py | 96 +++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 85 deletions(-) delete mode 100644 homeassistant/components/gios/model.py diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 858a756e3e3..895775495f9 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -4,15 +4,9 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - -from .model import GiosSensorEntityDescription - ATTRIBUTION: Final = "Data provided by GIOŚ" CONF_STATION_ID: Final = "station_id" -DEFAULT_NAME: Final = "GIOŚ" # Term of service GIOŚ allow downloading data no more than twice an hour. SCAN_INTERVAL: Final = timedelta(minutes=30) DOMAIN: Final = "gios" @@ -33,61 +27,3 @@ ATTR_PM10: Final = "pm10" ATTR_PM25: Final = "pm25" ATTR_SO2: Final = "so2" ATTR_AQI: Final = "aqi" - -SENSOR_TYPES: Final[tuple[GiosSensorEntityDescription, ...]] = ( - GiosSensorEntityDescription( - key=ATTR_AQI, - name="AQI", - device_class=SensorDeviceClass.AQI, - value=None, - ), - GiosSensorEntityDescription( - key=ATTR_C6H6, - name="C6H6", - icon="mdi:molecule", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_CO, - name="CO", - device_class=SensorDeviceClass.CO, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_NO2, - name="NO2", - device_class=SensorDeviceClass.NITROGEN_DIOXIDE, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_O3, - name="O3", - device_class=SensorDeviceClass.OZONE, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_PM10, - name="PM10", - device_class=SensorDeviceClass.PM10, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_PM25, - name="PM2.5", - device_class=SensorDeviceClass.PM25, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_SO2, - name="SO2", - device_class=SensorDeviceClass.SULPHUR_DIOXIDE, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), -) diff --git a/homeassistant/components/gios/model.py b/homeassistant/components/gios/model.py deleted file mode 100644 index 0f5d992590b..00000000000 --- a/homeassistant/components/gios/model.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Type definitions for GIOS integration.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class GiosSensorEntityDescription(SensorEntityDescription): - """Class describing GIOS sensor entities.""" - - value: Callable | None = round diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 391976ad793..2d32b8261f3 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -1,12 +1,25 @@ """Support for the GIOS service.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import logging from typing import Any, cast -from homeassistant.components.sensor import DOMAIN as PLATFORM, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN as PLATFORM, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_NAME, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_NAME, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONF_NAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType @@ -18,21 +31,90 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import GiosDataUpdateCoordinator from .const import ( ATTR_AQI, + ATTR_C6H6, + ATTR_CO, ATTR_INDEX, + ATTR_NO2, + ATTR_O3, + ATTR_PM10, ATTR_PM25, + ATTR_SO2, ATTR_STATION, ATTRIBUTION, - DEFAULT_NAME, DOMAIN, MANUFACTURER, - SENSOR_TYPES, URL, ) -from .model import GiosSensorEntityDescription _LOGGER = logging.getLogger(__name__) +@dataclass +class GiosSensorEntityDescription(SensorEntityDescription): + """Class describing GIOS sensor entities.""" + + value: Callable | None = round + + +SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( + GiosSensorEntityDescription( + key=ATTR_AQI, + name="AQI", + device_class=SensorDeviceClass.AQI, + value=None, + ), + GiosSensorEntityDescription( + key=ATTR_C6H6, + name="C6H6", + icon="mdi:molecule", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_CO, + name="CO", + device_class=SensorDeviceClass.CO, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_NO2, + name="NO2", + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_O3, + name="O3", + device_class=SensorDeviceClass.OZONE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_PM10, + name="PM10", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_PM25, + name="PM2.5", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_SO2, + name="SO2", + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -72,6 +154,7 @@ async def async_setup_entry( class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity): """Define an GIOS sensor.""" + _attr_has_entity_name = True entity_description: GiosSensorEntityDescription def __init__( @@ -86,10 +169,9 @@ class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, str(coordinator.gios.station_id))}, manufacturer=MANUFACTURER, - name=DEFAULT_NAME, + name=name, configuration_url=URL.format(station_id=coordinator.gios.station_id), ) - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" self._attrs: dict[str, Any] = { ATTR_ATTRIBUTION: ATTRIBUTION, From 8d88562d4025dd55309360d74a71ce23a6bdc052 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Jul 2022 15:27:20 +0200 Subject: [PATCH 489/820] Migrate Uptime to new entity naming style (#75090) --- homeassistant/components/uptime/sensor.py | 9 ++++++++- tests/components/uptime/test_sensor.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 944f9b77de8..3f7b7f5da25 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -12,6 +12,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -58,10 +60,15 @@ class UptimeSensor(SensorEntity): """Representation of an uptime sensor.""" _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_has_entity_name = True _attr_should_poll = False def __init__(self, entry: ConfigEntry) -> None: """Initialize the uptime sensor.""" - self._attr_name = entry.title self._attr_native_value = dt_util.utcnow() self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + name=entry.title, + identifiers={(DOMAIN, entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index e8d0306246f..053224c3b4f 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -2,9 +2,10 @@ import pytest from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.uptime.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -25,3 +26,11 @@ async def test_uptime_sensor( entry = entity_registry.async_get("sensor.uptime") assert entry assert entry.unique_id == init_integration.entry_id + + device_registry = dr.async_get(hass) + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, init_integration.entry_id)} + assert device_entry.name == init_integration.title + assert device_entry.entry_type == dr.DeviceEntryType.SERVICE From 393610c5348094ff571fecd50433d6f6efd300f0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Jul 2022 15:28:13 +0200 Subject: [PATCH 490/820] Migrate Season to new entity naming style (#75088) --- homeassistant/components/season/sensor.py | 9 ++++++++- tests/components/season/test_sensor.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 216475f0cdf..5fc161062bb 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -14,6 +14,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow @@ -121,13 +123,18 @@ class SeasonSensorEntity(SensorEntity): """Representation of the current season.""" _attr_device_class = "season__season" + _attr_has_entity_name = True def __init__(self, entry: ConfigEntry, hemisphere: str) -> None: """Initialize the season.""" - self._attr_name = entry.title self._attr_unique_id = entry.entry_id self.hemisphere = hemisphere self.type = entry.data[CONF_TYPE] + self._attr_device_info = DeviceInfo( + name=entry.title, + identifiers={(DOMAIN, entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) def update(self) -> None: """Update season.""" diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 90d01106bba..0c2470edb7b 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -4,7 +4,11 @@ from datetime import datetime from freezegun import freeze_time import pytest -from homeassistant.components.season.const import TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL +from homeassistant.components.season.const import ( + DOMAIN, + TYPE_ASTRONOMICAL, + TYPE_METEOROLOGICAL, +) from homeassistant.components.season.sensor import ( STATE_AUTUMN, STATE_SPRING, @@ -13,7 +17,7 @@ from homeassistant.components.season.sensor import ( ) from homeassistant.const import CONF_TYPE, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -123,6 +127,14 @@ async def test_season_southern_hemisphere( assert entry assert entry.unique_id == mock_config_entry.entry_id + device_registry = dr.async_get(hass) + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, mock_config_entry.entry_id)} + assert device_entry.name == "Season" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + async def test_season_equator( hass: HomeAssistant, From 952c90efcd5511c13936526259298e532aab1aa5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Jul 2022 08:45:09 -0600 Subject: [PATCH 491/820] Bump simplisafe-python to 2022.07.0 (#75294) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index a09c273076c..b6a139fba80 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2022.06.1"], + "requirements": ["simplisafe-python==2022.07.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 94759b214d4..363cfb2a733 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2171,7 +2171,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.06.1 +simplisafe-python==2022.07.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 397cc15e05c..21072433cc8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1446,7 +1446,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.06.1 +simplisafe-python==2022.07.0 # homeassistant.components.slack slackclient==2.5.0 From c52d4c64bf2a65f13955e8a8e779e2fe6b058d34 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 16 Jul 2022 16:57:17 +0200 Subject: [PATCH 492/820] Bump bimmer_connected to 0.10.1 (#75287) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index b10d4842163..0381035a63e 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.9.6"], + "requirements": ["bimmer_connected==0.10.1"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 363cfb2a733..63fd2862faa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 bellows==0.31.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.6 +bimmer_connected==0.10.1 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21072433cc8..034ead5c35e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -314,7 +314,7 @@ beautifulsoup4==4.11.1 bellows==0.31.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.6 +bimmer_connected==0.10.1 # homeassistant.components.bluetooth bleak==0.14.3 From 686449cef605eabfa00a34c1c25defdd6d54c284 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 16 Jul 2022 17:00:42 +0200 Subject: [PATCH 493/820] Force `_attr_native_value` to metric in bmw_connected_drive (#75225) Co-authored-by: rikroe --- .../components/bmw_connected_drive/coordinator.py | 3 ++- homeassistant/components/bmw_connected_drive/sensor.py | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index e5a968b47fd..08e90d3c4e0 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -33,7 +33,8 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator): entry.data[CONF_PASSWORD], get_region_from_name(entry.data[CONF_REGION]), observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), - use_metric_units=hass.config.units.is_metric, + # Force metric system as BMW API apparently only returns metric values now + use_metric_units=True, ) self.read_only = entry.options[CONF_READ_ONLY] self._entry = entry diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 9f19673c398..f1046881ed3 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -16,7 +16,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, @@ -183,10 +182,8 @@ class BMWSensor(BMWBaseEntity, SensorEntity): self._attr_name = f"{vehicle.name} {description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}" - if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._attr_native_unit_of_measurement = description.unit_imperial - else: - self._attr_native_unit_of_measurement = description.unit_metric + # Force metric system as BMW API apparently only returns metric values now + self._attr_native_unit_of_measurement = description.unit_metric @callback def _handle_coordinator_update(self) -> None: From 20d70337d51c0608d46cdfe17e5d8bd79be33947 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 16 Jul 2022 17:07:16 +0200 Subject: [PATCH 494/820] Migrate Trafikverket Weatherstation to new entity naming style (#75211) --- homeassistant/components/trafikverket_weatherstation/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index c54c9f67388..68c47e9320c 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -165,6 +165,7 @@ class TrafikverketWeatherStation( entity_description: TrafikverketSensorEntityDescription _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -176,7 +177,6 @@ class TrafikverketWeatherStation( """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{sensor_station} {description.name}" self._attr_unique_id = f"{entry_id}_{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, From 859189421beb13bc787b61bc4b588f548ac3dbf8 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 16 Jul 2022 17:43:47 +0200 Subject: [PATCH 495/820] Migrate BraviaTV to new entity naming style (#75253) --- .coveragerc | 1 + homeassistant/components/braviatv/const.py | 1 - homeassistant/components/braviatv/entity.py | 29 ++++++++++++++++ .../components/braviatv/media_player.py | 31 +++-------------- homeassistant/components/braviatv/remote.py | 33 +++---------------- 5 files changed, 38 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/braviatv/entity.py diff --git a/.coveragerc b/.coveragerc index cfc51dc700d..0a2e8e57fa8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -137,6 +137,7 @@ omit = homeassistant/components/bosch_shc/switch.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py + homeassistant/components/braviatv/entity.py homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/remote.py homeassistant/components/broadlink/__init__.py diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 01746cbe963..4aa44992cbf 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -12,6 +12,5 @@ CONF_IGNORED_SOURCES: Final = "ignored_sources" BRAVIA_CONFIG_FILE: Final = "bravia.conf" CLIENTID_PREFIX: Final = "HomeAssistant" -DEFAULT_NAME: Final = f"{ATTR_MANUFACTURER} Bravia TV" DOMAIN: Final = "braviatv" NICKNAME: Final = "Home Assistant" diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py new file mode 100644 index 00000000000..ad896ae8c5a --- /dev/null +++ b/homeassistant/components/braviatv/entity.py @@ -0,0 +1,29 @@ +"""A entity class for BraviaTV integration.""" +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import BraviaTVCoordinator +from .const import ATTR_MANUFACTURER, DOMAIN + + +class BraviaTVEntity(CoordinatorEntity[BraviaTVCoordinator]): + """BraviaTV entity class.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: BraviaTVCoordinator, + unique_id: str, + model: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model=model, + name=f"{ATTR_MANUFACTURER} {model}", + ) diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 745325a4c39..5d812788563 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -9,12 +9,10 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BraviaTVCoordinator -from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN +from .const import DOMAIN +from .entity import BraviaTVEntity async def async_setup_entry( @@ -27,19 +25,13 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id assert unique_id is not None - device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - manufacturer=ATTR_MANUFACTURER, - model=config_entry.title, - name=DEFAULT_NAME, - ) async_add_entities( - [BraviaTVMediaPlayer(coordinator, DEFAULT_NAME, unique_id, device_info)] + [BraviaTVMediaPlayer(coordinator, unique_id, config_entry.title)] ) -class BraviaTVMediaPlayer(CoordinatorEntity[BraviaTVCoordinator], MediaPlayerEntity): +class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Representation of a Bravia TV Media Player.""" _attr_device_class = MediaPlayerDeviceClass.TV @@ -57,21 +49,6 @@ class BraviaTVMediaPlayer(CoordinatorEntity[BraviaTVCoordinator], MediaPlayerEnt | MediaPlayerEntityFeature.STOP ) - def __init__( - self, - coordinator: BraviaTVCoordinator, - name: str, - unique_id: str, - device_info: DeviceInfo, - ) -> None: - """Initialize the entity.""" - - self._attr_device_info = device_info - self._attr_name = name - self._attr_unique_id = unique_id - - super().__init__(coordinator) - @property def state(self) -> str | None: """Return the state of the device.""" diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 016f8363b09..f45b2d74004 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -7,12 +7,10 @@ from typing import Any from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BraviaTVCoordinator -from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN +from .const import DOMAIN +from .entity import BraviaTVEntity async def async_setup_entry( @@ -25,36 +23,13 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id assert unique_id is not None - device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - manufacturer=ATTR_MANUFACTURER, - model=config_entry.title, - name=DEFAULT_NAME, - ) - async_add_entities( - [BraviaTVRemote(coordinator, DEFAULT_NAME, unique_id, device_info)] - ) + async_add_entities([BraviaTVRemote(coordinator, unique_id, config_entry.title)]) -class BraviaTVRemote(CoordinatorEntity[BraviaTVCoordinator], RemoteEntity): +class BraviaTVRemote(BraviaTVEntity, RemoteEntity): """Representation of a Bravia TV Remote.""" - def __init__( - self, - coordinator: BraviaTVCoordinator, - name: str, - unique_id: str, - device_info: DeviceInfo, - ) -> None: - """Initialize the entity.""" - - self._attr_device_info = device_info - self._attr_name = name - self._attr_unique_id = unique_id - - super().__init__(coordinator) - @property def is_on(self) -> bool: """Return true if device is on.""" From ecc219fbc14158f5925627c83bb6d63d4a3a0ca9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Jul 2022 11:02:08 -0500 Subject: [PATCH 496/820] Include the source in the bluetooth service info (#75112) --- homeassistant/components/bluetooth/__init__.py | 14 ++++++++++---- homeassistant/helpers/service_info/bluetooth.py | 4 +++- tests/components/bluetooth/test_init.py | 8 +++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 01f55048c6d..b6058767669 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -39,6 +39,8 @@ _LOGGER = logging.getLogger(__name__) MAX_REMEMBER_ADDRESSES: Final = 2048 +SOURCE_LOCAL: Final = "local" + class BluetoothCallbackMatcherOptional(TypedDict, total=False): """Matcher for the bluetooth integration for callback optional fields.""" @@ -266,7 +268,7 @@ class BluetoothManager: ): if service_info is None: service_info = BluetoothServiceInfo.from_advertisement( - device, advertisement_data + device, advertisement_data, SOURCE_LOCAL ) try: callback(service_info, BluetoothChange.ADVERTISEMENT) @@ -277,7 +279,7 @@ class BluetoothManager: return if service_info is None: service_info = BluetoothServiceInfo.from_advertisement( - device, advertisement_data + device, advertisement_data, SOURCE_LOCAL ) for domain in matched_domains: discovery_flow.async_create_flow( @@ -312,7 +314,9 @@ class BluetoothManager: ): try: callback( - BluetoothServiceInfo.from_advertisement(*device_adv_data), + BluetoothServiceInfo.from_advertisement( + *device_adv_data, SOURCE_LOCAL + ), BluetoothChange.ADVERTISEMENT, ) except Exception: # pylint: disable=broad-except @@ -338,7 +342,9 @@ class BluetoothManager: discovered = models.HA_BLEAK_SCANNER.discovered_devices history = models.HA_BLEAK_SCANNER.history return [ - BluetoothServiceInfo.from_advertisement(*history[device.address]) + BluetoothServiceInfo.from_advertisement( + *history[device.address], SOURCE_LOCAL + ) for device in discovered if device.address in history ] diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index 9dff2782da0..003a4228d80 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -22,10 +22,11 @@ class BluetoothServiceInfo(BaseServiceInfo): manufacturer_data: dict[int, bytes] service_data: dict[str, bytes] service_uuids: list[str] + source: str @classmethod def from_advertisement( - cls, device: BLEDevice, advertisement_data: AdvertisementData + cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str ) -> BluetoothServiceInfo: """Create a BluetoothServiceInfo from an advertisement.""" return cls( @@ -35,6 +36,7 @@ class BluetoothServiceInfo(BaseServiceInfo): manufacturer_data=advertisement_data.manufacturer_data, service_data=advertisement_data.service_data, service_uuids=advertisement_data.service_uuids, + source=source, ) @cached_property diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 2bbe4ce7dcb..1d348f42f0e 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -6,6 +6,7 @@ from bleak.backends.scanner import AdvertisementData, BLEDevice from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( + SOURCE_LOCAL, BluetoothChange, BluetoothServiceInfo, models, @@ -244,6 +245,7 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): assert len(service_infos) == 1 # wrong_name should not appear because bleak no longer sees it assert service_infos[0].name == "wohand" + assert service_infos[0].source == SOURCE_LOCAL assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True @@ -255,7 +257,8 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): callbacks = [] def _fake_subscriber( - service_info: BluetoothServiceInfo, change: BluetoothChange + service_info: BluetoothServiceInfo, + change: BluetoothChange, ) -> None: """Fake subscriber for the BleakScanner.""" callbacks.append((service_info, change)) @@ -312,16 +315,19 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): service_info: BluetoothServiceInfo = callbacks[0][0] assert service_info.name == "wohand" + assert service_info.source == SOURCE_LOCAL assert service_info.manufacturer == "Nordic Semiconductor ASA" assert service_info.manufacturer_id == 89 service_info: BluetoothServiceInfo = callbacks[1][0] assert service_info.name == "empty" + assert service_info.source == SOURCE_LOCAL assert service_info.manufacturer is None assert service_info.manufacturer_id is None service_info: BluetoothServiceInfo = callbacks[2][0] assert service_info.name == "empty" + assert service_info.source == SOURCE_LOCAL assert service_info.manufacturer is None assert service_info.manufacturer_id is None From b9c8d65940ec47a82332b8b1a67301da018ccadf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Jul 2022 12:49:15 -0500 Subject: [PATCH 497/820] Restore accessory state into pairing using new HKC methods (#75276) --- .../components/homekit_controller/__init__.py | 23 ++-- .../homekit_controller/config_flow.py | 27 ++-- .../homekit_controller/connection.py | 118 ++++++++++-------- .../homekit_controller/device_trigger.py | 5 +- .../homekit_controller/diagnostics.py | 1 + .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/storage.py | 12 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/common.py | 6 +- .../homekit_controller/test_config_flow.py | 6 +- .../homekit_controller/test_diagnostics.py | 2 + .../homekit_controller/test_init.py | 12 +- .../homekit_controller/test_storage.py | 3 +- 14 files changed, 130 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 9175dca50bc..37dd648dedb 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -6,6 +6,11 @@ import logging from typing import Any import aiohomekit +from aiohomekit.exceptions import ( + AccessoryDisconnectedError, + AccessoryNotFoundError, + EncryptionError, +) from aiohomekit.model import Accessory from aiohomekit.model.characteristics import ( Characteristic, @@ -26,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice, valid_serial_number from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS -from .storage import EntityMapStorage +from .storage import async_get_entity_storage from .utils import async_get_controller, folded_name _LOGGER = logging.getLogger(__name__) @@ -227,23 +232,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, unique_id=normalize_hkid(conn.unique_id) ) - if not await conn.async_setup(): + try: + await conn.async_setup() + except (AccessoryNotFoundError, EncryptionError, AccessoryDisconnectedError) as ex: del hass.data[KNOWN_DEVICES][conn.unique_id] - if (connection := getattr(conn.pairing, "connection", None)) and hasattr( - connection, "host" - ): - raise ConfigEntryNotReady( - f"Cannot connect to {connection.host}:{connection.port}" - ) - raise ConfigEntryNotReady + await conn.pairing.close() + raise ConfigEntryNotReady from ex return True async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up for Homekit devices.""" - map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) - await map_storage.async_initialize() + await async_get_entity_storage(hass) await async_get_controller(hass) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 41094a65e00..9b8b759f80e 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -8,7 +8,6 @@ from typing import Any import aiohomekit from aiohomekit.controller.abstract import AbstractPairing from aiohomekit.exceptions import AuthenticationError -from aiohomekit.model import Accessories, CharacteristicsTypes, ServicesTypes from aiohomekit.utils import domain_supported, domain_to_name import voluptuous as vol @@ -20,6 +19,7 @@ from homeassistant.helpers import device_registry as dr from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES +from .storage import async_get_entity_storage from .utils import async_get_controller HOMEKIT_DIR = ".homekit" @@ -252,9 +252,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid ) - self.hass.async_create_task( - conn.async_refresh_entity_map_and_entities(config_num) - ) + conn.async_notify_config_changed(config_num) return self.async_abort(reason="already_configured") _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) @@ -481,17 +479,22 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # available. Otherwise request a fresh copy from the API. # This removes the 'accessories' key from pairing_data at # the same time. - if not (accessories := pairing_data.pop("accessories", None)): - accessories = await pairing.list_accessories_and_characteristics() - - parsed = Accessories.from_list(accessories) - accessory_info = parsed.aid(1).services.first( - service_type=ServicesTypes.ACCESSORY_INFORMATION - ) - name = accessory_info.value(CharacteristicsTypes.NAME, "") + name = await pairing.get_primary_name() await pairing.close() + # Save the state of the accessories so we do not + # have to request them again when we setup the + # config entry. + accessories_state = pairing.accessories_state + entity_storage = await async_get_entity_storage(self.hass) + assert self.unique_id is not None + entity_storage.async_create_or_update_map( + self.unique_id, + accessories_state.config_num, + accessories_state.accessories.serialize(), + ) + return self.async_create_entry(title=name, data=pairing_data) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 85e8e3987bd..7e4dcc59be0 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Callable import datetime import logging +from types import MappingProxyType from typing import Any from aiohomekit import Controller @@ -17,8 +18,9 @@ from aiohomekit.model import Accessories, Accessory from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.services import Service +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_VIA_DEVICE -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo @@ -62,7 +64,12 @@ def valid_serial_number(serial: str) -> bool: class HKDevice: """HomeKit device.""" - def __init__(self, hass, config_entry, pairing_data) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + pairing_data: MappingProxyType[str, Any], + ) -> None: """Initialise a generic HomeKit device.""" self.hass = hass @@ -78,11 +85,6 @@ class HKDevice: self.pairing_data["AccessoryPairingID"], self.pairing_data ) - self.accessories: list[Any] | None = None - self.config_num = 0 - - self.entity_map = Accessories() - # A list of callbacks that turn HK accessories into entities self.accessory_factories: list[AddAccessoryCb] = [] @@ -132,6 +134,17 @@ class HKDevice: self.watchable_characteristics: list[tuple[int, int]] = [] self.pairing.dispatcher_connect(self.process_new_events) + self.pairing.dispatcher_connect_config_changed(self.process_config_changed) + + @property + def entity_map(self) -> Accessories: + """Return the accessories from the pairing.""" + return self.pairing.accessories_state.accessories + + @property + def config_num(self) -> int: + """Return the config num from the pairing.""" + return self.pairing.accessories_state.config_num def add_pollable_characteristics( self, characteristics: list[tuple[int, int]] @@ -169,13 +182,13 @@ class HKDevice: self.available = available async_dispatcher_send(self.hass, self.signal_state_updated) - async def async_ensure_available(self) -> bool: + async def async_ensure_available(self) -> None: """Verify the accessory is available after processing the entity map.""" if self.available: - return True + return if self.watchable_characteristics and self.pollable_characteristics: # We already tried, no need to try again - return False + return # We there are no watchable and not pollable characteristics, # we need to force a connection to the device to verify its alive. # @@ -185,34 +198,42 @@ class HKDevice: primary = self.entity_map.accessories[0] aid = primary.aid iid = primary.accessory_information[CharacteristicsTypes.SERIAL_NUMBER].iid - try: - await self.pairing.get_characteristics([(aid, iid)]) - except (AccessoryDisconnectedError, EncryptionError, AccessoryNotFoundError): - return False + await self.pairing.get_characteristics([(aid, iid)]) self.async_set_available_state(True) - return True - async def async_setup(self) -> bool: + async def async_setup(self) -> None: """Prepare to use a paired HomeKit device in Home Assistant.""" entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] if cache := entity_storage.get_map(self.unique_id): - self.accessories = cache["accessories"] - self.config_num = cache["config_num"] - self.entity_map = Accessories.from_list(self.accessories) - elif not await self.async_refresh_entity_map(self.config_num): - return False + self.pairing.restore_accessories_state( + cache["accessories"], cache["config_num"] + ) + + # We need to force an update here to make sure we have + # the latest values since the async_update we do in + # async_process_entity_map will no values to poll yet + # since entities are added via dispatching and then + # they add the chars they are concerned about in + # async_added_to_hass which is too late. + # + # Ideally we would know which entities we are about to add + # so we only poll those chars but that is not possible + # yet. + await self.pairing.async_populate_accessories_state(force_update=True) await self.async_process_entity_map() - if not await self.async_ensure_available(): - return False + await self.async_ensure_available() + + if not cache: + # If its missing from the cache, make sure we save it + self.async_save_entity_map() # If everything is up to date, we can create the entities # since we know the data is not stale. await self.async_add_new_entities() self._polling_interval_remover = async_track_time_interval( self.hass, self.async_update, DEFAULT_SCAN_INTERVAL ) - return True async def async_add_new_entities(self) -> None: """Add new entities to Home Assistant.""" @@ -390,9 +411,6 @@ class HKDevice: # Ensure the Pairing object has access to the latest version of the entity map. This # is especially important for BLE, as the Pairing instance relies on the entity map # to map aid/iid to GATT characteristics. So push it to there as well. - - self.pairing.pairing_data["accessories"] = self.accessories # type: ignore[attr-defined] - self.async_detect_workarounds() # Migrate to new device ids @@ -403,13 +421,6 @@ class HKDevice: # Load any triggers for this config entry await async_setup_triggers_for_entry(self.hass, self.config_entry) - if self.watchable_characteristics: - await self.pairing.subscribe(self.watchable_characteristics) - if not self.pairing.is_connected: - return - - await self.async_update() - async def async_unload(self) -> None: """Stop interacting with device and prepare for removal from hass.""" if self._polling_interval_remover: @@ -421,34 +432,31 @@ class HKDevice: self.config_entry, self.platforms ) - async def async_refresh_entity_map_and_entities(self, config_num: int) -> None: - """Refresh the entity map and entities for this pairing.""" - await self.async_refresh_entity_map(config_num) + def async_notify_config_changed(self, config_num: int) -> None: + """Notify the pairing of a config change.""" + self.pairing.notify_config_changed(config_num) + + def process_config_changed(self, config_num: int) -> None: + """Handle a config change notification from the pairing.""" + self.hass.async_create_task(self.async_update_new_accessories_state()) + + async def async_update_new_accessories_state(self) -> None: + """Process a change in the pairings accessories state.""" + self.async_save_entity_map() await self.async_process_entity_map() + if self.watchable_characteristics: + await self.pairing.subscribe(self.watchable_characteristics) + await self.async_update() await self.async_add_new_entities() - async def async_refresh_entity_map(self, config_num: int) -> bool: - """Handle setup of a HomeKit accessory.""" - try: - self.accessories = await self.pairing.list_accessories_and_characteristics() - except AccessoryDisconnectedError: - # If we fail to refresh this data then we will naturally retry - # later when Bonjour spots c# is still not up to date. - return False - - assert self.accessories is not None - - self.entity_map = Accessories.from_list(self.accessories) - + @callback + def async_save_entity_map(self) -> None: + """Save the entity map.""" entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] - entity_storage.async_create_or_update_map( - self.unique_id, config_num, self.accessories + self.unique_id, self.config_num, self.entity_map.serialize() ) - self.config_num = config_num - return True - def add_accessory_factory(self, add_entities_cb) -> None: """Add a callback to run when discovering new entities for accessories.""" self.accessory_factories.append(add_entities_cb) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 49924b30c57..d7828753d98 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -15,6 +15,7 @@ from homeassistant.components.automation import ( AutomationTriggerInfo, ) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.typing import ConfigType @@ -195,7 +196,9 @@ TRIGGER_FINDERS = { } -async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): +async def async_setup_triggers_for_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Triggers aren't entities as they have no state, but we still need to set them up for a config entry.""" hkid = config_entry.data["AccessoryPairingID"] conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] diff --git a/homeassistant/components/homekit_controller/diagnostics.py b/homeassistant/components/homekit_controller/diagnostics.py index f83ce7604cf..9b17c0c2fe7 100644 --- a/homeassistant/components/homekit_controller/diagnostics.py +++ b/homeassistant/components/homekit_controller/diagnostics.py @@ -107,6 +107,7 @@ def _async_get_diagnostics( # It is roughly equivalent to what is in .storage/homekit_controller-entity-map # But it also has the latest values seen by the polling or events data["entity-map"] = accessories = connection.entity_map.serialize() + data["config-num"] = connection.config_num # It contains serial numbers, which we should strip out for accessory in accessories: diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 0275afa07fe..a5ca76fdc1e 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.0.0"], + "requirements": ["aiohomekit==1.1.1"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index ff39c52627e..8c0628c97f6 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -7,7 +7,7 @@ from typing import Any, TypedDict from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store -from .const import DOMAIN +from .const import DOMAIN, ENTITY_MAP ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map" ENTITY_MAP_STORAGE_VERSION = 1 @@ -91,3 +91,13 @@ class EntityMapStorage: def _data_to_save(self) -> StorageLayout: """Return data of entity map to store in a file.""" return StorageLayout(pairings=self.storage_data) + + +async def async_get_entity_storage(hass: HomeAssistant) -> EntityMapStorage: + """Get entity storage.""" + if ENTITY_MAP in hass.data: + map_storage: EntityMapStorage = hass.data[ENTITY_MAP] + return map_storage + map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) + await map_storage.async_initialize() + return map_storage diff --git a/requirements_all.txt b/requirements_all.txt index 63fd2862faa..4fc5e180e95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.0.0 +aiohomekit==1.1.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 034ead5c35e..ff34193cee8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.0.0 +aiohomekit==1.1.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index e773b2ffc66..18367d28f63 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -9,7 +9,7 @@ import os from typing import Any, Final from unittest import mock -from aiohomekit.model import Accessories, Accessory +from aiohomekit.model import Accessories, AccessoriesState, Accessory from aiohomekit.testing import FakeController, FakePairing from homeassistant.components import zeroconf @@ -225,7 +225,9 @@ async def device_config_changed(hass, accessories): accessories_obj = Accessories() for accessory in accessories: accessories_obj.add_accessory(accessory) - pairing.accessories = accessories_obj + pairing._accessories_state = AccessoriesState( + accessories_obj, pairing.config_num + 1 + ) discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 9535b7d0cd5..73bd159fd73 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -2,7 +2,7 @@ import asyncio from unittest import mock import unittest.mock -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import aiohomekit from aiohomekit.exceptions import AuthenticationError @@ -492,7 +492,7 @@ async def test_discovery_already_configured_update_csharp(hass, controller): connection_mock = AsyncMock() connection_mock.pairing.connect.reconnect_soon = AsyncMock() - connection_mock.async_refresh_entity_map = AsyncMock() + connection_mock.async_notify_config_changed = MagicMock() hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} device = setup_mock_accessory(controller) @@ -515,7 +515,7 @@ async def test_discovery_already_configured_update_csharp(hass, controller): assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryPort"] == discovery_info.port - assert connection_mock.async_refresh_entity_map_and_entities.await_count == 1 + assert connection_mock.async_notify_config_changed.call_count == 1 @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index e770279752e..dd0e35b0d1e 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -28,6 +28,7 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "version": 1, "data": {"AccessoryPairingID": "00:00:00:00:00:00"}, }, + "config-num": 0, "entity-map": [ { "aid": 1, @@ -299,6 +300,7 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "version": 1, "data": {"AccessoryPairingID": "00:00:00:00:00:00"}, }, + "config-num": 0, "entity-map": [ { "aid": 1, diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 57b39076aa6..df14ad325b6 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -111,8 +111,16 @@ async def test_offline_device_raises(hass, controller): nonlocal is_connected return is_connected - def get_characteristics(self, chars, *args, **kwargs): - raise AccessoryDisconnectedError("any") + async def async_populate_accessories_state(self, *args, **kwargs): + nonlocal is_connected + if not is_connected: + raise AccessoryDisconnectedError("any") + + async def get_characteristics(self, chars, *args, **kwargs): + nonlocal is_connected + if not is_connected: + raise AccessoryDisconnectedError("any") + return {} with patch("aiohomekit.testing.FakePairing", OfflineFakePairing): await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index b4ed617f901..13d613e3916 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -3,6 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.homekit_controller.const import ENTITY_MAP +from homeassistant.components.homekit_controller.storage import EntityMapStorage from tests.common import flush_store from tests.components.homekit_controller.common import ( @@ -68,7 +69,7 @@ async def test_storage_is_updated_on_add(hass, hass_storage, utcnow): """Test entity map storage is cleaned up on adding an accessory.""" await setup_test_component(hass, create_lightbulb_service) - entity_map = hass.data[ENTITY_MAP] + entity_map: EntityMapStorage = hass.data[ENTITY_MAP] hkid = "00:00:00:00:00:00" # Is in memory store updated? From 514e826fed5bd601223022920a3bde38a09b8b6c Mon Sep 17 00:00:00 2001 From: Jelte Zeilstra Date: Sat, 16 Jul 2022 20:39:11 +0200 Subject: [PATCH 498/820] Add install UniFi device update feature (#75302) * Add install UniFi device update feature * Add tests for install UniFi device update feature * Fix type error * Process review feedback * Process review feedback --- homeassistant/components/unifi/update.py | 13 +- tests/components/unifi/test_update.py | 165 +++++++++++++++-------- 2 files changed, 118 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 09720f15f84..24967e043d9 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.update import ( DOMAIN, @@ -71,7 +72,6 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): DOMAIN = DOMAIN TYPE = DEVICE_UPDATE _attr_device_class = UpdateDeviceClass.FIRMWARE - _attr_supported_features = UpdateEntityFeature.PROGRESS def __init__(self, device, controller): """Set up device update entity.""" @@ -79,6 +79,11 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): self.device = self._item + self._attr_supported_features = UpdateEntityFeature.PROGRESS + + if self.controller.site_role == "admin": + self._attr_supported_features |= UpdateEntityFeature.INSTALL + @property def name(self) -> str: """Return the name of the device.""" @@ -126,3 +131,9 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): async def options_updated(self) -> None: """No action needed.""" + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + await self.controller.api.devices.upgrade(self.device.mac) diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index b5eb9c1d02e..7bbeb65497e 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -1,23 +1,59 @@ """The tests for the UniFi Network update platform.""" +from copy import deepcopy from aiounifi.controller import MESSAGE_DEVICE from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING +from yarl import URL +from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN from homeassistant.components.update import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, UpdateDeviceClass, + UpdateEntityFeature, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) -from .test_controller import setup_unifi_integration +from .test_controller import DESCRIPTION, setup_unifi_integration + +DEVICE_1 = { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device 1", + "next_interval": 20, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + "upgrade_to_firmware": "4.3.17.11279", +} + +DEVICE_2 = { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.1.2", + "mac": "00:00:00:00:01:02", + "model": "US16P150", + "name": "Device 2", + "next_interval": 20, + "state": 0, + "type": "usw", + "version": "4.0.42.10433", +} async def test_no_entities(hass, aioclient_mock): @@ -31,41 +67,11 @@ async def test_device_updates( hass, aioclient_mock, mock_unifi_websocket, mock_device_registry ): """Test the update_items function with some devices.""" - device_1 = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device 1", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - "upgrade_to_firmware": "4.3.17.11279", - } - device_2 = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "ip": "10.0.1.2", - "mac": "00:00:00:00:01:02", - "model": "US16P150", - "name": "Device 2", - "next_interval": 20, - "state": 0, - "type": "usw", - "version": "4.0.42.10433", - } + device_1 = deepcopy(DEVICE_1) await setup_unifi_integration( hass, aioclient_mock, - devices_response=[device_1, device_2], + devices_response=[device_1, DEVICE_2], ) assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 2 @@ -76,6 +82,10 @@ async def test_device_updates( assert device_1_state.attributes[ATTR_LATEST_VERSION] == "4.3.17.11279" assert device_1_state.attributes[ATTR_IN_PROGRESS] is False assert device_1_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert ( + device_1_state.attributes[ATTR_SUPPORTED_FEATURES] + == UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL + ) device_2_state = hass.states.get("update.device_2") assert device_2_state.state == STATE_OFF @@ -83,6 +93,10 @@ async def test_device_updates( assert device_2_state.attributes[ATTR_LATEST_VERSION] == "4.0.42.10433" assert device_2_state.attributes[ATTR_IN_PROGRESS] is False assert device_2_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert ( + device_2_state.attributes[ATTR_SUPPORTED_FEATURES] + == UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL + ) # Simulate start of update @@ -122,46 +136,79 @@ async def test_device_updates( assert device_1_state.attributes[ATTR_IN_PROGRESS] is False -async def test_controller_state_change( - hass, aioclient_mock, mock_unifi_websocket, mock_device_registry -): - """Verify entities state reflect on controller becoming unavailable.""" - device = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - "upgrade_to_firmware": "4.3.17.11279", - } +async def test_not_admin(hass, aioclient_mock): + """Test that the INSTALL feature is not available on a non-admin account.""" + description = deepcopy(DESCRIPTION) + description[0]["site_role"] = "not admin" await setup_unifi_integration( hass, aioclient_mock, - devices_response=[device], + site_description=description, + devices_response=[DEVICE_1], ) assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 - assert hass.states.get("update.device").state == STATE_ON + device_state = hass.states.get("update.device_1") + assert device_state.state == STATE_ON + assert ( + device_state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature.PROGRESS + ) + + +async def test_install(hass, aioclient_mock): + """Test the device update install call.""" + config_entry = await setup_unifi_integration( + hass, aioclient_mock, devices_response=[DEVICE_1] + ) + + assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 + device_state = hass.states.get("update.device_1") + assert device_state.state == STATE_ON + + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + url = f"https://{controller.host}:1234/api/s/{controller.site}/cmd/devmgr" + aioclient_mock.clear_requests() + aioclient_mock.post(url) + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.device_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0] == ( + "post", + URL(url), + {"cmd": "upgrade", "mac": "00:00:00:00:01:01"}, + {}, + ) + + +async def test_controller_state_change( + hass, aioclient_mock, mock_unifi_websocket, mock_device_registry +): + """Verify entities state reflect on controller becoming unavailable.""" + await setup_unifi_integration( + hass, + aioclient_mock, + devices_response=[DEVICE_1], + ) + + assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 + assert hass.states.get("update.device_1").state == STATE_ON # Controller unavailable mock_unifi_websocket(state=STATE_DISCONNECTED) await hass.async_block_till_done() - assert hass.states.get("update.device").state == STATE_UNAVAILABLE + assert hass.states.get("update.device_1").state == STATE_UNAVAILABLE # Controller available mock_unifi_websocket(state=STATE_RUNNING) await hass.async_block_till_done() - assert hass.states.get("update.device").state == STATE_ON + assert hass.states.get("update.device_1").state == STATE_ON From ae4b1967a31decc87a71301d93d3d65bc6ef9300 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 16 Jul 2022 20:56:48 +0200 Subject: [PATCH 499/820] Use pydeconz interface controls for lights (#75261) --- homeassistant/components/deconz/gateway.py | 8 ++-- homeassistant/components/deconz/light.py | 44 ++++++++++--------- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 0ee967f88d9..f8e4548cf91 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -10,8 +10,8 @@ from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors from pydeconz.interfaces import sensors -from pydeconz.interfaces.api import APIItems, GroupedAPIItems -from pydeconz.interfaces.groups import Groups +from pydeconz.interfaces.api_handlers import APIHandler, GroupedAPIHandler +from pydeconz.interfaces.groups import GroupHandler from pydeconz.models.event import EventType from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry @@ -127,7 +127,7 @@ class DeconzGateway: def register_platform_add_device_callback( self, add_device_callback: Callable[[EventType, str], None], - deconz_device_interface: APIItems | GroupedAPIItems, + deconz_device_interface: APIHandler | GroupedAPIHandler, always_ignore_clip_sensors: bool = False, ) -> None: """Wrap add_device_callback to check allow_new_devices option.""" @@ -148,7 +148,7 @@ class DeconzGateway: self.ignored_devices.add((async_add_device, device_id)) return - if isinstance(deconz_device_interface, Groups): + if isinstance(deconz_device_interface, GroupHandler): self.deconz_groups.add((async_add_device, device_id)) if not self.option_allow_deconz_groups: return diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 7be6551cccc..7c6f1a0e362 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -3,16 +3,12 @@ from __future__ import annotations from typing import Any, Generic, TypedDict, TypeVar +from pydeconz.interfaces.groups import GroupHandler +from pydeconz.interfaces.lights import LightHandler from pydeconz.models import ResourceType from pydeconz.models.event import EventType from pydeconz.models.group import Group -from pydeconz.models.light import ( - ALERT_LONG, - ALERT_SHORT, - EFFECT_COLOR_LOOP, - EFFECT_NONE, -) -from pydeconz.models.light.light import Light +from pydeconz.models.light.light import Light, LightAlert, LightColorMode, LightEffect from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -42,8 +38,14 @@ from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_GROUP = "is_deconz_group" -EFFECT_TO_DECONZ = {EFFECT_COLORLOOP: EFFECT_COLOR_LOOP, "None": EFFECT_NONE} -FLASH_TO_DECONZ = {FLASH_SHORT: ALERT_SHORT, FLASH_LONG: ALERT_LONG} +EFFECT_TO_DECONZ = {EFFECT_COLORLOOP: LightEffect.COLOR_LOOP, "None": LightEffect.NONE} +FLASH_TO_DECONZ = {FLASH_SHORT: LightAlert.SHORT, FLASH_LONG: LightAlert.LONG} + +DECONZ_TO_COLOR_MODE = { + LightColorMode.CT: ColorMode.COLOR_TEMP, + LightColorMode.HS: ColorMode.HS, + LightColorMode.XY: ColorMode.XY, +} _L = TypeVar("_L", Group, Light) @@ -51,10 +53,10 @@ _L = TypeVar("_L", Group, Light) class SetStateAttributes(TypedDict, total=False): """Attributes available with set state call.""" - alert: str + alert: LightAlert brightness: int color_temperature: int - effect: str + effect: LightEffect hue: int on: bool saturation: int @@ -130,7 +132,13 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): """Set up light.""" super().__init__(device, gateway) - self._attr_supported_color_modes: set[str] = set() + self.api: GroupHandler | LightHandler + if isinstance(self._device, Light): + self.api = self.gateway.api.lights.lights + elif isinstance(self._device, Group): + self.api = self.gateway.api.groups + + self._attr_supported_color_modes: set[ColorMode] = set() if device.color_temp is not None: self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) @@ -158,12 +166,8 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): @property def color_mode(self) -> str | None: """Return the color mode of the light.""" - if self._device.color_mode == "ct": - color_mode = ColorMode.COLOR_TEMP - elif self._device.color_mode == "hs": - color_mode = ColorMode.HS - elif self._device.color_mode == "xy": - color_mode = ColorMode.XY + if self._device.color_mode in DECONZ_TO_COLOR_MODE: + color_mode = DECONZ_TO_COLOR_MODE[self._device.color_mode] elif self._device.brightness is not None: color_mode = ColorMode.BRIGHTNESS else: @@ -229,7 +233,7 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): if ATTR_EFFECT in kwargs and kwargs[ATTR_EFFECT] in EFFECT_TO_DECONZ: data["effect"] = EFFECT_TO_DECONZ[kwargs[ATTR_EFFECT]] - await self._device.set_state(**data) + await self.api.set_state(id=self._device.resource_id, **data) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" @@ -246,7 +250,7 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): data["alert"] = FLASH_TO_DECONZ[kwargs[ATTR_FLASH]] del data["on"] - await self._device.set_state(**data) + await self.api.set_state(id=self._device.resource_id, **data) @property def extra_state_attributes(self) -> dict[str, bool]: diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index c1b0b07de02..8019d0df2df 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==99"], + "requirements": ["pydeconz==100"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 4fc5e180e95..ff2640568d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==99 +pydeconz==100 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff34193cee8..fc576e28612 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==99 +pydeconz==100 # homeassistant.components.dexcom pydexcom==0.2.3 From cb12f77e330d40cd39e640e649ed9d246b6654c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Jul 2022 22:00:58 +0200 Subject: [PATCH 500/820] Update sentry-sdk to 1.7.2 (#75331) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 29b897e4a3c..c1163ea1791 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.7.1"], + "requirements": ["sentry-sdk==1.7.2"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index ff2640568d8..706cf2b1a6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.7.1 +sentry-sdk==1.7.2 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc576e28612..b0a4d4acff5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1434,7 +1434,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.7.1 +sentry-sdk==1.7.2 # homeassistant.components.sharkiq sharkiq==0.0.1 From 08d648799710b3b5696b86bbc42ec256d8b727fa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Jul 2022 14:04:44 -0600 Subject: [PATCH 501/820] Handle (and better log) more AirVisual cloud API errors (#75332) --- .../components/airvisual/__init__.py | 11 ++++----- .../components/airvisual/config_flow.py | 4 +++- .../components/airvisual/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/airvisual/test_config_flow.py | 24 +++++++++++++++++++ 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index c1bb6020b3c..a2a3d76c3db 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta from math import ceil -from typing import Any, cast +from typing import Any from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( @@ -12,6 +12,7 @@ from pyairvisual.errors import ( InvalidKeyError, KeyExpiredError, NodeProError, + UnauthorizedError, ) from homeassistant.config_entries import ConfigEntry @@ -210,9 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: - data = await api_coro - return cast(dict[str, Any], data) - except (InvalidKeyError, KeyExpiredError) as ex: + return await api_coro + except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex: raise ConfigEntryAuthFailed from ex except AirVisualError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err @@ -253,8 +253,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with NodeSamba( entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD] ) as node: - data = await node.async_get_latest_measurements() - return cast(dict[str, Any], data) + return await node.async_get_latest_measurements() except NodeProError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index f97616c38fc..385c9f55753 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -9,8 +9,10 @@ from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( AirVisualError, InvalidKeyError, + KeyExpiredError, NodeProError, NotFoundError, + UnauthorizedError, ) import voluptuous as vol @@ -119,7 +121,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input[CONF_API_KEY] not in valid_keys: try: await coro - except InvalidKeyError: + except (InvalidKeyError, KeyExpiredError, UnauthorizedError): errors[CONF_API_KEY] = "invalid_api_key" except NotFoundError: errors[CONF_CITY] = "location_not_found" diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index ed803a3e6a1..9a6279f34a6 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,7 +3,7 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==5.0.9"], + "requirements": ["pyairvisual==2022.07.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyairvisual", "pysmb"] diff --git a/requirements_all.txt b/requirements_all.txt index 706cf2b1a6b..ef293dcdcae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1369,7 +1369,7 @@ pyaftership==21.11.0 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.9 +pyairvisual==2022.07.0 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0a4d4acff5..2b1ae82fd16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -935,7 +935,7 @@ pyaehw4a1==0.3.9 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.9 +pyairvisual==2022.07.0 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 12a9e67122f..f97ee845dba 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -4,8 +4,10 @@ from unittest.mock import patch from pyairvisual.errors import ( AirVisualError, InvalidKeyError, + KeyExpiredError, NodeProError, NotFoundError, + UnauthorizedError, ) import pytest @@ -84,6 +86,28 @@ async def test_duplicate_error(hass, config, config_entry, data): {CONF_API_KEY: "invalid_api_key"}, INTEGRATION_TYPE_GEOGRAPHY_NAME, ), + ( + { + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + }, + KeyExpiredError, + {CONF_API_KEY: "invalid_api_key"}, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + ), + ( + { + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + }, + UnauthorizedError, + {CONF_API_KEY: "invalid_api_key"}, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + ), ( { CONF_API_KEY: "abcde12345", From 4ceda65889ff0cd8e3959f4d3df386cab8f87838 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 16 Jul 2022 22:26:22 +0200 Subject: [PATCH 502/820] Update pyotgw to 2.0.0 (#75285) * Update pyotgw to 2.0.0 * Include updated tests --- .../components/opentherm_gw/__init__.py | 4 +- .../components/opentherm_gw/config_flow.py | 4 +- .../components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../opentherm_gw/test_config_flow.py | 42 +++++++++++++------ tests/components/opentherm_gw/test_init.py | 4 +- 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 76f8341734c..7c27eeceede 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -416,7 +416,7 @@ class OpenThermGatewayDevice: self.status = {} self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" - self.gateway = pyotgw.pyotgw() + self.gateway = pyotgw.OpenThermGateway() self.gw_version = None async def cleanup(self, event=None): @@ -427,7 +427,7 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - self.status = await self.gateway.connect(self.hass.loop, self.device_path) + self.status = await self.gateway.connect(self.device_path) version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) self.gw_version = version_string[18:] if version_string else None _LOGGER.debug( diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 1d66d6e2069..3f91496adab 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -59,8 +59,8 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def test_connection(): """Try to connect to the OpenTherm Gateway.""" - otgw = pyotgw.pyotgw() - status = await otgw.connect(self.hass.loop, device) + otgw = pyotgw.OpenThermGateway() + status = await otgw.connect(device) await otgw.disconnect() return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 7aa19224020..dfb60413721 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==1.1b1"], + "requirements": ["pyotgw==2.0.0"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ef293dcdcae..42360e81184 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1718,7 +1718,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==1.1b1 +pyotgw==2.0.0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b1ae82fd16..8d6fa41d253 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1173,7 +1173,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==1.1b1 +pyotgw==2.0.0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 8244ece6b9f..46d53bc54b5 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -43,10 +43,12 @@ async def test_form_user(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=None - ) as mock_pyotgw_disconnect: + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) @@ -75,10 +77,12 @@ async def test_form_import(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=None - ) as mock_pyotgw_disconnect: + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -117,10 +121,12 @@ async def test_form_duplicate_entries(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=None - ) as mock_pyotgw_disconnect: + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result1 = await hass.config_entries.flow.async_configure( flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) @@ -148,8 +154,10 @@ async def test_form_connection_timeout(hass): ) with patch( - "pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError) - ) as mock_connect: + "pyotgw.OpenThermGateway.connect", side_effect=(asyncio.TimeoutError) + ) as mock_connect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"}, @@ -166,7 +174,11 @@ async def test_form_connection_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect: + with patch( + "pyotgw.OpenThermGateway.connect", side_effect=(SerialException) + ) as mock_connect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) @@ -196,7 +208,11 @@ async def test_options_migration(hass): with patch( "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe", return_value=True, - ), patch("homeassistant.components.opentherm_gw.async_setup", return_value=True): + ), patch( + "homeassistant.components.opentherm_gw.async_setup", return_value=True + ), patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/opentherm_gw/test_init.py b/tests/components/opentherm_gw/test_init.py index 554f58fd81b..7e16805c683 100644 --- a/tests/components/opentherm_gw/test_init.py +++ b/tests/components/opentherm_gw/test_init.py @@ -34,7 +34,7 @@ async def test_device_registry_insert(hass): with patch( "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", return_value=None, - ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS): + ), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() @@ -62,7 +62,7 @@ async def test_device_registry_update(hass): with patch( "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", return_value=None, - ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS_UPD): + ), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS_UPD): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() From 2f92c47fe368329d0905b9cb5f0288f7de442eb9 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 17 Jul 2022 05:07:47 +0800 Subject: [PATCH 503/820] Apply filter to libav.hls logging namespace (#75330) --- homeassistant/components/stream/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index ef68ea7bcae..f0b4ed99654 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -187,14 +187,15 @@ def filter_libav_logging() -> None: return logging.getLogger(__name__).isEnabledFor(logging.DEBUG) for logging_namespace in ( - "libav.mp4", + "libav.NULL", "libav.h264", "libav.hevc", + "libav.hls", + "libav.mp4", + "libav.mpegts", "libav.rtsp", "libav.tcp", "libav.tls", - "libav.mpegts", - "libav.NULL", ): logging.getLogger(logging_namespace).addFilter(libav_filter) From 59c99e0d60d21b8bc110a436038227c399282671 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 16 Jul 2022 18:28:17 -0400 Subject: [PATCH 504/820] Improve UniFi Protect unauth handling (#75269) --- homeassistant/components/unifiprotect/data.py | 12 ++++++--- tests/components/unifiprotect/test_init.py | 27 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 9e0783a99b1..d4140759a7b 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -72,6 +72,7 @@ class ProtectData: self._pending_camera_ids: set[str] = set() self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None + self._auth_failures = 0 self.last_update_success = False self.api = protect @@ -117,9 +118,13 @@ class ProtectData: try: updates = await self.api.update(force=force) except NotAuthorized: - await self.async_stop() - _LOGGER.exception("Reauthentication required") - self._entry.async_start_reauth(self._hass) + if self._auth_failures < 10: + _LOGGER.exception("Auth error while updating") + self._auth_failures += 1 + else: + await self.async_stop() + _LOGGER.exception("Reauthentication required") + self._entry.async_start_reauth(self._hass) self.last_update_success = False except ClientError: if self.last_update_success: @@ -129,6 +134,7 @@ class ProtectData: self._async_process_updates(self.api.bootstrap) else: self.last_update_success = True + self._auth_failures = 0 self._async_process_updates(updates) @callback diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index f6f0645df18..9392caa30ac 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -9,14 +9,18 @@ import aiohttp from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect.data import NVR, Bootstrap, Light -from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN +from homeassistant.components.unifiprotect.const import ( + CONF_DISABLE_RTSP, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import _patch_discovery -from .utils import MockUFPFixture, init_entry +from .utils import MockUFPFixture, init_entry, time_changed from tests.common import MockConfigEntry @@ -145,12 +149,23 @@ async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture): async def test_setup_failed_update_reauth(hass: HomeAssistant, ufp: MockUFPFixture): """Test setup of unifiprotect entry with update that gives unauthroized error.""" - ufp.api.update = AsyncMock(side_effect=NotAuthorized) - await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert ufp.entry.state == ConfigEntryState.SETUP_RETRY - assert ufp.api.update.called + assert ufp.entry.state == ConfigEntryState.LOADED + + # reauth should not be triggered until there are 10 auth failures in a row + # to verify it is not transient + ufp.api.update = AsyncMock(side_effect=NotAuthorized) + for _ in range(10): + await time_changed(hass, DEFAULT_SCAN_INTERVAL) + assert len(hass.config_entries.flow._progress) == 0 + + assert ufp.api.update.call_count == 10 + assert ufp.entry.state == ConfigEntryState.LOADED + + await time_changed(hass, DEFAULT_SCAN_INTERVAL) + assert ufp.api.update.call_count == 11 + assert len(hass.config_entries.flow._progress) == 1 async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture): From 79a09409321308da522df9e0a652dfd7b5fca1b6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 17 Jul 2022 00:25:56 +0000 Subject: [PATCH 505/820] [ci skip] Translation update --- .../accuweather/translations/pt.json | 3 ++ .../components/airtouch4/translations/pt.json | 11 +++++++ .../airvisual/translations/sensor.pt.json | 6 ++++ .../alarm_control_panel/translations/pt.json | 1 + .../components/apple_tv/translations/pt.json | 2 +- .../components/axis/translations/pt.json | 1 + .../azure_devops/translations/pt.json | 3 +- .../binary_sensor/translations/pt.json | 3 +- .../components/blebox/translations/pt.json | 1 + .../components/bond/translations/pt.json | 1 + .../components/co2signal/translations/pt.json | 15 +++++++++- .../components/elkm1/translations/pt.json | 6 ++-- .../evil_genius_labs/translations/pt.json | 3 +- .../components/generic/translations/pt.json | 1 + .../components/group/translations/pt.json | 15 +++++++++- .../homeassistant/translations/pt.json | 1 + .../components/homekit/translations/pt.json | 5 ++++ .../components/hue/translations/pt.json | 3 ++ .../components/meater/translations/pt.json | 1 + .../components/netgear/translations/pt.json | 7 +++++ .../overkiz/translations/sensor.id.json | 3 +- .../components/recorder/translations/pt.json | 8 +++++ .../components/scrape/translations/pt.json | 12 +++++++- .../components/season/translations/pt.json | 9 +++++- .../season/translations/sensor.pt.json | 4 +-- .../components/sense/translations/pt.json | 3 ++ .../components/sensor/translations/he.json | 2 ++ .../components/sensor/translations/pt.json | 2 +- .../components/sia/translations/pt.json | 10 +++++++ .../components/sql/translations/pt.json | 30 +++++++++++++++++++ .../components/timer/translations/pt.json | 2 +- .../ukraine_alarm/translations/pt.json | 7 +++++ .../components/verisure/translations/id.json | 13 +++++++- .../components/verisure/translations/pl.json | 15 +++++++++- .../xiaomi_aqara/translations/pt.json | 2 +- .../zodiac/translations/sensor.pt.json | 12 ++++---- 36 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/airtouch4/translations/pt.json create mode 100644 homeassistant/components/recorder/translations/pt.json create mode 100644 homeassistant/components/sql/translations/pt.json create mode 100644 homeassistant/components/ukraine_alarm/translations/pt.json diff --git a/homeassistant/components/accuweather/translations/pt.json b/homeassistant/components/accuweather/translations/pt.json index 08d419ec9b2..8b5d307e722 100644 --- a/homeassistant/components/accuweather/translations/pt.json +++ b/homeassistant/components/accuweather/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, + "create_entry": { + "default": "Alguns sensores n\u00e3o s\u00e3o ativados por defeito. Podem ser ativados no registo da entidade ap\u00f3s a configura\u00e7\u00e3o da integra\u00e7\u00e3o.\nA previs\u00e3o do tempo n\u00e3o est\u00e1 ativada por defeito. Pode ativ\u00e1-la nas op\u00e7\u00f5es de integra\u00e7\u00e3o." + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_api_key": "Chave de API inv\u00e1lida" diff --git a/homeassistant/components/airtouch4/translations/pt.json b/homeassistant/components/airtouch4/translations/pt.json new file mode 100644 index 00000000000..4e8578a0a28 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Anfitri\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.pt.json b/homeassistant/components/airvisual/translations/sensor.pt.json index 08a2e0c018e..f7a976b51ed 100644 --- a/homeassistant/components/airvisual/translations/sensor.pt.json +++ b/homeassistant/components/airvisual/translations/sensor.pt.json @@ -1,5 +1,11 @@ { "state": { + "airvisual__pollutant_label": { + "co": "Mon\u00f3xido de carbono", + "n2": "Di\u00f3xido de nitrog\u00e9nio", + "o3": "Ozono", + "p1": "PM10" + }, "airvisual__pollutant_level": { "moderate": "Moderado" } diff --git a/homeassistant/components/alarm_control_panel/translations/pt.json b/homeassistant/components/alarm_control_panel/translations/pt.json index e4293b81731..fb8cce10c93 100644 --- a/homeassistant/components/alarm_control_panel/translations/pt.json +++ b/homeassistant/components/alarm_control_panel/translations/pt.json @@ -13,6 +13,7 @@ "armed_custom_bypass": "Armado com desvio personalizado", "armed_home": "Armado Casa", "armed_night": "Armado noite", + "armed_vacation": "Armado f\u00e9rias", "arming": "A armar", "disarmed": "Desarmado", "disarming": "A desarmar", diff --git a/homeassistant/components/apple_tv/translations/pt.json b/homeassistant/components/apple_tv/translations/pt.json index deec60a19ea..e54e421caa5 100644 --- a/homeassistant/components/apple_tv/translations/pt.json +++ b/homeassistant/components/apple_tv/translations/pt.json @@ -40,7 +40,7 @@ "data": { "device_input": "Dispositivo" }, - "description": "Comece por introduzir o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que pretende adicionar. Se algum dispositivo foi automaticamente encontrado na sua rede, ele \u00e9 mostrado abaixo.\n\nSe n\u00e3o conseguir ver o seu dispositivo ou se tiver algum problema, tente especificar o endere\u00e7o IP do dispositivo.\n\n{devices}", + "description": "Comece por introduzir o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que pretende adicionar. Se algum dispositivo foi automaticamente encontrado na sua rede, ele \u00e9 mostrado abaixo.\n\nSe n\u00e3o conseguir ver o seu dispositivo ou se tiver algum problema, tente especificar o endere\u00e7o IP do dispositivo.", "title": "Configure uma nova Apple TV" } } diff --git a/homeassistant/components/axis/translations/pt.json b/homeassistant/components/axis/translations/pt.json index 8ba642263a4..b74ecb2dc44 100644 --- a/homeassistant/components/axis/translations/pt.json +++ b/homeassistant/components/axis/translations/pt.json @@ -10,6 +10,7 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/pt.json b/homeassistant/components/azure_devops/translations/pt.json index 2af1f548447..b09f2cceda7 100644 --- a/homeassistant/components/azure_devops/translations/pt.json +++ b/homeassistant/components/azure_devops/translations/pt.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" - } + }, + "flow_title": "{project_url}" } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index 470acfff5db..9eba64372d4 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -186,7 +186,8 @@ "on": "Detectado" }, "update": { - "off": "Actualizado" + "off": "Actualizado", + "on": "Atualiza\u00e7\u00e3o dispon\u00edvel" }, "vibration": { "off": "Limpo", diff --git a/homeassistant/components/blebox/translations/pt.json b/homeassistant/components/blebox/translations/pt.json index 9c2be6fd04b..8b581a984e7 100644 --- a/homeassistant/components/blebox/translations/pt.json +++ b/homeassistant/components/blebox/translations/pt.json @@ -8,6 +8,7 @@ "unknown": "Erro inesperado", "unsupported_version": "O dispositivo BleBox possui firmware desatualizado. Atualize-o primeiro." }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/bond/translations/pt.json b/homeassistant/components/bond/translations/pt.json index 2173d698932..828e7c55baf 100644 --- a/homeassistant/components/bond/translations/pt.json +++ b/homeassistant/components/bond/translations/pt.json @@ -8,6 +8,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/co2signal/translations/pt.json b/homeassistant/components/co2signal/translations/pt.json index cf6bc7f5bc4..6d105e40d36 100644 --- a/homeassistant/components/co2signal/translations/pt.json +++ b/homeassistant/components/co2signal/translations/pt.json @@ -6,8 +6,21 @@ "step": { "coordinates": { "data": { - "latitude": "Latitude" + "latitude": "Latitude", + "longitude": "Longitude" } + }, + "country": { + "data": { + "country_code": "C\u00f3digo do Pa\u00eds" + } + }, + "user": { + "data": { + "api_key": "Token de Acesso", + "location": "Obter dados para" + }, + "description": "Visite https://co2signal.com/ para solicitar um token." } } } diff --git a/homeassistant/components/elkm1/translations/pt.json b/homeassistant/components/elkm1/translations/pt.json index 6b770832a24..08fe97d2354 100644 --- a/homeassistant/components/elkm1/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt.json @@ -10,12 +10,14 @@ "data": { "password": "Palavra-passe", "username": "Nome de Utilizador" - } + }, + "title": "Ligar ao Controlo Elk-M1" }, "manual_connection": { "data": { "username": "Nome de Utilizador" - } + }, + "title": "Ligar ao Controlo Elk-M1" } } } diff --git a/homeassistant/components/evil_genius_labs/translations/pt.json b/homeassistant/components/evil_genius_labs/translations/pt.json index f13cad90edc..bf245f20e6e 100644 --- a/homeassistant/components/evil_genius_labs/translations/pt.json +++ b/homeassistant/components/evil_genius_labs/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "timeout": "Tempo limite para estabelecer liga\u00e7\u00e3o" }, "step": { "user": { diff --git a/homeassistant/components/generic/translations/pt.json b/homeassistant/components/generic/translations/pt.json index b59623f6607..06abd0a7dfa 100644 --- a/homeassistant/components/generic/translations/pt.json +++ b/homeassistant/components/generic/translations/pt.json @@ -13,6 +13,7 @@ "step": { "init": { "data": { + "authentication": "Autentica\u00e7\u00e3o", "password": "Palavra-passe", "username": "Nome de Utilizador" } diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 57c8a41e630..0d0e11d7ffe 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -1,10 +1,23 @@ { "config": { "step": { + "binary_sensor": { + "title": "Adicionar Grupo" + }, + "fan": { + "title": "Adicionar Grupo" + }, + "light": { + "title": "Adicionar Grupo" + }, "media_player": { "data": { "entities": "Membros" - } + }, + "title": "Adicionar Grupo" + }, + "switch": { + "title": "Adicionar Grupo" } } }, diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json index 13fd384d6a2..30f84823c6b 100644 --- a/homeassistant/components/homeassistant/translations/pt.json +++ b/homeassistant/components/homeassistant/translations/pt.json @@ -10,6 +10,7 @@ "os_version": "Vers\u00e3o do Sistema Operativo", "python_version": "Vers\u00e3o Python", "timezone": "Fuso hor\u00e1rio", + "user": "Utilizador", "version": "Vers\u00e3o", "virtualenv": "Ambiente Virtual" } diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json index 4e25e3b691c..931dfcdd1c9 100644 --- a/homeassistant/components/homekit/translations/pt.json +++ b/homeassistant/components/homekit/translations/pt.json @@ -20,6 +20,11 @@ "cameras": { "title": "Selecione o codec de v\u00eddeo da c\u00e2mera." }, + "exclude": { + "data": { + "entities": "Entidades" + } + }, "init": { "data": { "domains": "Dom\u00ednios a incluir", diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index 8f51f8d74e9..9b982d6c84e 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -36,6 +36,9 @@ "trigger_subtype": { "button_1": "Primeiro bot\u00e3o", "button_4": "Quarto bot\u00e3o" + }, + "trigger_type": { + "short_release": "Bot\u00e3o \"{subtype}\" solto ap\u00f3s press\u00e3o curta" } }, "options": { diff --git a/homeassistant/components/meater/translations/pt.json b/homeassistant/components/meater/translations/pt.json index 69bade43888..bc859189b94 100644 --- a/homeassistant/components/meater/translations/pt.json +++ b/homeassistant/components/meater/translations/pt.json @@ -11,6 +11,7 @@ }, "user": { "data": { + "password": "Palavra-passe", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/netgear/translations/pt.json b/homeassistant/components/netgear/translations/pt.json index ce8a9287272..ece60e5e010 100644 --- a/homeassistant/components/netgear/translations/pt.json +++ b/homeassistant/components/netgear/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.id.json b/homeassistant/components/overkiz/translations/sensor.id.json index bf4703507f8..0d0cc6ab7ed 100644 --- a/homeassistant/components/overkiz/translations/sensor.id.json +++ b/homeassistant/components/overkiz/translations/sensor.id.json @@ -39,7 +39,8 @@ }, "overkiz__three_way_handle_direction": { "closed": "Tutup", - "open": "Buka" + "open": "Buka", + "tilt": "Miring" } } } \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/pt.json b/homeassistant/components/recorder/translations/pt.json new file mode 100644 index 00000000000..b06f3d16e60 --- /dev/null +++ b/homeassistant/components/recorder/translations/pt.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "database_engine": "Motor de Base de Dados", + "database_version": "Vers\u00e3o de Base de Dados" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/pt.json b/homeassistant/components/scrape/translations/pt.json index 00c65c09cdf..003da0eed66 100644 --- a/homeassistant/components/scrape/translations/pt.json +++ b/homeassistant/components/scrape/translations/pt.json @@ -6,7 +6,17 @@ "step": { "user": { "data": { - "name": "Nome" + "name": "Nome", + "unit_of_measurement": "Unidade de Medida" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unidade de Medida" } } } diff --git a/homeassistant/components/season/translations/pt.json b/homeassistant/components/season/translations/pt.json index b7bb07e9522..c8d0ddf9f70 100644 --- a/homeassistant/components/season/translations/pt.json +++ b/homeassistant/components/season/translations/pt.json @@ -1,7 +1,14 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "type": "Defini\u00e7\u00e3o do tipo de esta\u00e7\u00e3o" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.pt.json b/homeassistant/components/season/translations/sensor.pt.json index e30461da7d2..3f157f43c79 100644 --- a/homeassistant/components/season/translations/sensor.pt.json +++ b/homeassistant/components/season/translations/sensor.pt.json @@ -1,9 +1,9 @@ { "state": { "season__season": { - "autumn": "Outono ", + "autumn": "Outono", "spring": "Primavera", - "summer": "Ver\u00e3o ", + "summer": "Ver\u00e3o", "winter": "Inverno" }, "season__season__": { diff --git a/homeassistant/components/sense/translations/pt.json b/homeassistant/components/sense/translations/pt.json index d429d0af1e8..1b8b4e2dc18 100644 --- a/homeassistant/components/sense/translations/pt.json +++ b/homeassistant/components/sense/translations/pt.json @@ -10,6 +10,9 @@ }, "step": { "reauth_validate": { + "data": { + "password": "Palavra-passe" + }, "title": "Reautenticar integra\u00e7\u00e3o" }, "user": { diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index 819da5aad0c..7f2dd33a023 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -2,10 +2,12 @@ "device_automation": { "condition_type": { "is_apparent_power": "\u05d4\u05e2\u05d5\u05e6\u05de\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} \u05de\u05e1\u05ea\u05de\u05e0\u05ea", + "is_battery_level": "\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea \u05e9\u05dc {entity_name}", "is_reactive_power": "\u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}" }, "trigger_type": { "apparent_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05dc\u05db\u05d0\u05d5\u05e8\u05d4", + "battery_level": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4", "reactive_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9" } }, diff --git a/homeassistant/components/sensor/translations/pt.json b/homeassistant/components/sensor/translations/pt.json index 0e162b9c7ca..864d49f2373 100644 --- a/homeassistant/components/sensor/translations/pt.json +++ b/homeassistant/components/sensor/translations/pt.json @@ -18,7 +18,7 @@ "illuminance": "ilumin\u00e2ncia {entity_name}", "power": "pot\u00eancia {entity_name}", "pressure": "press\u00e3o {entity_name}", - "signal_strength": "for\u00e7a do sinal de {entity_name}", + "signal_strength": "Altera\u00e7\u00e3o da intensidade do sinal de {entity_name}", "temperature": "temperatura de {entity_name}", "value": "valor {entity_name}" } diff --git a/homeassistant/components/sia/translations/pt.json b/homeassistant/components/sia/translations/pt.json index 0077ceddd46..21f8130bfc5 100644 --- a/homeassistant/components/sia/translations/pt.json +++ b/homeassistant/components/sia/translations/pt.json @@ -7,5 +7,15 @@ } } } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignorar a verifica\u00e7\u00e3o de carimbo de data/hora dos eventos SIA", + "zones": "N\u00famero de zonas da conta" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sql/translations/pt.json b/homeassistant/components/sql/translations/pt.json new file mode 100644 index 00000000000..f2cf791e9fe --- /dev/null +++ b/homeassistant/components/sql/translations/pt.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "data": { + "unit_of_measurement": "Unidade de Medida" + }, + "data_description": { + "unit_of_measurement": "Unidade de Medida (opcional)" + } + } + } + }, + "options": { + "error": { + "query_invalid": "Busca SQL In\u00e1lida" + }, + "step": { + "init": { + "data": { + "column": "Coluna", + "unit_of_measurement": "Unidade de Medida" + }, + "data_description": { + "unit_of_measurement": "Unidade de Medida (opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/timer/translations/pt.json b/homeassistant/components/timer/translations/pt.json index a49163aed8c..1f506228973 100644 --- a/homeassistant/components/timer/translations/pt.json +++ b/homeassistant/components/timer/translations/pt.json @@ -1,7 +1,7 @@ { "state": { "_": { - "active": "ativo", + "active": "Ativo", "idle": "Em espera", "paused": "Em pausa" } diff --git a/homeassistant/components/ukraine_alarm/translations/pt.json b/homeassistant/components/ukraine_alarm/translations/pt.json new file mode 100644 index 00000000000..e6c831ab07c --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "timeout": "Tempo limite para estabelecer liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/id.json b/homeassistant/components/verisure/translations/id.json index 5c9badda341..c8b78a7282a 100644 --- a/homeassistant/components/verisure/translations/id.json +++ b/homeassistant/components/verisure/translations/id.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autentikasi tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" + "unknown": "Kesalahan yang tidak diharapkan", + "unknown_mfa": "Terjadi kesalahan yang tidak diketahui dalam pengaturan MFA" }, "step": { "installation": { @@ -15,6 +16,11 @@ }, "description": "Home Assistant menemukan beberapa instalasi Verisure di akun My Pages. Pilih instalasi untuk ditambahkan ke Home Assistant." }, + "mfa": { + "data": { + "code": "Kode Verifikasi" + } + }, "reauth_confirm": { "data": { "description": "Autentikasi ulang dengan akun Verisure My Pages Anda.", @@ -22,6 +28,11 @@ "password": "Kata Sandi" } }, + "reauth_mfa": { + "data": { + "code": "Kode Verifikasi" + } + }, "user": { "data": { "description": "Masuk dengan akun Verisure My Pages Anda.", diff --git a/homeassistant/components/verisure/translations/pl.json b/homeassistant/components/verisure/translations/pl.json index baaf61f60c3..dcdf566f9d4 100644 --- a/homeassistant/components/verisure/translations/pl.json +++ b/homeassistant/components/verisure/translations/pl.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unknown_mfa": "Podczas konfigurowania us\u0142ugi MFA wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant znalaz\u0142 wiele instalacji Verisure na Twoim koncie. Wybierz instalacj\u0119, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistanta." }, + "mfa": { + "data": { + "code": "Kod weryfikacyjny", + "description": "Twoje konto ma w\u0142\u0105czon\u0105 weryfikacj\u0119 dwuetapow\u0105. Wprowad\u017a kod weryfikacyjny wys\u0142any przez Verisure." + } + }, "reauth_confirm": { "data": { "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Verisure.", @@ -22,6 +29,12 @@ "password": "Has\u0142o" } }, + "reauth_mfa": { + "data": { + "code": "Kod weryfikacyjny", + "description": "Twoje konto ma w\u0142\u0105czon\u0105 weryfikacj\u0119 dwuetapow\u0105. Wprowad\u017a kod weryfikacyjny wys\u0142any przez Verisure." + } + }, "user": { "data": { "description": "Zaloguj si\u0119 na swoje konto Verisure.", diff --git a/homeassistant/components/xiaomi_aqara/translations/pt.json b/homeassistant/components/xiaomi_aqara/translations/pt.json index a800e4d57c6..1b7f8e1c0b9 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt.json @@ -17,7 +17,7 @@ "data": { "name": "Nome da Gateway" }, - "description": "A chave (palavra-passe) pode ser recuperada usando este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se a chave n\u00e3o for fornecida, apenas os sensores estar\u00e3o acess\u00edveis" + "description": "A chave (palavra-passe) pode ser obtida usando este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se a chave n\u00e3o for fornecida, apenas os sensores estar\u00e3o acess\u00edveis" }, "user": { "data": { diff --git a/homeassistant/components/zodiac/translations/sensor.pt.json b/homeassistant/components/zodiac/translations/sensor.pt.json index b37a6886df0..a5e943a5daf 100644 --- a/homeassistant/components/zodiac/translations/sensor.pt.json +++ b/homeassistant/components/zodiac/translations/sensor.pt.json @@ -1,16 +1,16 @@ { "state": { "zodiac__sign": { - "aquarius": "Aqu\u00e1rio ", - "aries": "Carneiro ", - "cancer": "Caranguejo ", + "aquarius": "Aqu\u00e1rio", + "aries": "Carneiro", + "cancer": "Caranguejo", "capricorn": "Capric\u00f3rnio", - "gemini": "G\u00e9meos ", - "leo": "Le\u00e3o ", + "gemini": "G\u00e9meos", + "leo": "Le\u00e3o", "libra": "Balan\u00e7a", "pisces": "Peixes", "sagittarius": "Sagit\u00e1rio", - "scorpio": "Escorpi\u00e3o ", + "scorpio": "Escorpi\u00e3o", "taurus": "Touro", "virgo": "Virgem" } From ba8a530d19dc3e93602275961de46d700611f4a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Jul 2022 23:14:23 -0500 Subject: [PATCH 506/820] Use shared bluetooth models for BluetoothServiceInfo (#75322) --- .../helpers/service_info/bluetooth.py | 62 +------------------ homeassistant/package_constraints.txt | 1 + pyproject.toml | 1 + requirements.txt | 1 + 4 files changed, 6 insertions(+), 59 deletions(-) diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index 003a4228d80..d4d74a45be4 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -1,60 +1,4 @@ """The bluetooth integration service info.""" -from __future__ import annotations - -import dataclasses -from functools import cached_property -from typing import TYPE_CHECKING - -from homeassistant.data_entry_flow import BaseServiceInfo - -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - from bleak.backends.scanner import AdvertisementData - - -@dataclasses.dataclass -class BluetoothServiceInfo(BaseServiceInfo): - """Prepared info from bluetooth entries.""" - - name: str - address: str - rssi: int - manufacturer_data: dict[int, bytes] - service_data: dict[str, bytes] - service_uuids: list[str] - source: str - - @classmethod - def from_advertisement( - cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str - ) -> BluetoothServiceInfo: - """Create a BluetoothServiceInfo from an advertisement.""" - return cls( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=device.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - source=source, - ) - - @cached_property - def manufacturer(self) -> str | None: - """Convert manufacturer data to a string.""" - from bleak.backends.device import ( # pylint: disable=import-outside-toplevel - MANUFACTURERS, - ) - - for manufacturer in self.manufacturer_data: - if manufacturer in MANUFACTURERS: - name: str = MANUFACTURERS[manufacturer] - return name - return None - - @cached_property - def manufacturer_id(self) -> int | None: - """Get the first manufacturer id.""" - for manufacturer in self.manufacturer_data: - return manufacturer - return None +from home_assistant_bluetooth import ( # pylint: disable=unused-import # noqa: F401 + BluetoothServiceInfo, +) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d19b95fa2d3..0d81d485829 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,6 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 +home-assistant-bluetooth==1.3.0 home-assistant-frontend==20220707.1 httpx==0.23.0 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index 41059773977..9b4c99c1152 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.23.0", + "home-assistant-bluetooth==1.3.0", "ifaddr==0.1.7", "jinja2==3.1.2", "lru-dict==1.1.8", diff --git a/requirements.txt b/requirements.txt index 84d8711753e..ea29bc59435 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 httpx==0.23.0 +home-assistant-bluetooth==1.3.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 From 2eebda63fdde17072b126c0d4b2de3d9c86b7cfa Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 17 Jul 2022 22:00:01 +1000 Subject: [PATCH 507/820] Correct docstrings in Advantage Air (#75344) --- .../components/advantage_air/binary_sensor.py | 14 +++++++------- homeassistant/components/advantage_air/cover.py | 4 ++-- homeassistant/components/advantage_air/select.py | 4 ++-- homeassistant/components/advantage_air/switch.py | 2 +- tests/components/advantage_air/test_climate.py | 2 +- tests/components/advantage_air/test_cover.py | 2 +- tests/components/advantage_air/test_select.py | 4 ++-- tests/components/advantage_air/test_switch.py | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index bc302e5d4a6..c87bf37ca92 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -21,7 +21,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up AdvantageAir motion platform.""" + """Set up AdvantageAir Binary Sensor platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] @@ -39,13 +39,13 @@ async def async_setup_entry( class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): - """Advantage Air Filter.""" + """Advantage Air Filter sensor.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key): - """Initialize an Advantage Air Filter.""" + """Initialize an Advantage Air Filter sensor.""" super().__init__(instance, ac_key) self._attr_name = f'{self._ac["name"]} filter' self._attr_unique_id = ( @@ -59,12 +59,12 @@ class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): - """Advantage Air Zone Motion.""" + """Advantage Air Zone Motion sensor.""" _attr_device_class = BinarySensorDeviceClass.MOTION def __init__(self, instance, ac_key, zone_key): - """Initialize an Advantage Air Zone Motion.""" + """Initialize an Advantage Air Zone Motion sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} motion' self._attr_unique_id = ( @@ -78,13 +78,13 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): - """Advantage Air Zone MyZone.""" + """Advantage Air Zone MyZone sensor.""" _attr_entity_registry_enabled_default = False _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): - """Initialize an Advantage Air Zone MyZone.""" + """Initialize an Advantage Air Zone MyZone sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} myZone' self._attr_unique_id = ( diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index f8240126476..391f39953d2 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -40,7 +40,7 @@ async def async_setup_entry( class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): - """Advantage Air Cover Class.""" + """Advantage Air Zone Vent.""" _attr_device_class = CoverDeviceClass.DAMPER _attr_supported_features = ( @@ -50,7 +50,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): ) def __init__(self, instance, ac_key, zone_key): - """Initialize an Advantage Air Cover Class.""" + """Initialize an Advantage Air Zone Vent.""" super().__init__(instance, ac_key, zone_key) self._attr_name = self._zone["name"] self._attr_unique_id = ( diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index ff2a555bc80..0edae258279 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -15,7 +15,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up AdvantageAir toggle platform.""" + """Set up AdvantageAir select platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] @@ -49,7 +49,7 @@ class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): @property def current_option(self): - """Return the fresh air status.""" + """Return the current MyZone.""" return self._number_to_name[self._ac["myZone"]] async def async_select_option(self, option): diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 44be859aa63..3c0060c65b3 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -17,7 +17,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up AdvantageAir toggle platform.""" + """Set up AdvantageAir switch platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 27f89d1df3e..47073f27fc1 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -34,7 +34,7 @@ from tests.components.advantage_air import ( async def test_climate_async_setup_entry(hass, aioclient_mock): - """Test climate setup.""" + """Test climate platform.""" aioclient_mock.get( TEST_SYSTEM_URL, diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index fa3abda5138..c638c6a3c87 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -27,7 +27,7 @@ from tests.components.advantage_air import ( async def test_cover_async_setup_entry(hass, aioclient_mock): - """Test climate setup without sensors.""" + """Test cover platform.""" aioclient_mock.get( TEST_SYSTEM_URL, diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index 41ed9c407fc..2ba982fc384 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -19,7 +19,7 @@ from tests.components.advantage_air import ( async def test_select_async_setup_entry(hass, aioclient_mock): - """Test climate setup without sensors.""" + """Test select platform.""" aioclient_mock.get( TEST_SYSTEM_URL, @@ -36,7 +36,7 @@ async def test_select_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 - # Test Select Entity + # Test MyZone Select Entity entity_id = "select.testname_ac_one_myzone" state = hass.states.get(entity_id) assert state diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py index 1d99d7f29c1..41af9e8ff80 100644 --- a/tests/components/advantage_air/test_switch.py +++ b/tests/components/advantage_air/test_switch.py @@ -23,7 +23,7 @@ from tests.components.advantage_air import ( async def test_cover_async_setup_entry(hass, aioclient_mock): - """Test climate setup without sensors.""" + """Test switch platform.""" aioclient_mock.get( TEST_SYSTEM_URL, From 9a27f1437d84e1ab87a8089f910fc14e384581c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 07:25:19 -0500 Subject: [PATCH 508/820] Use default encoder when saving storage (#75319) --- homeassistant/util/json.py | 27 ++++++-------------- tests/helpers/test_storage.py | 48 ++++++++++++++++++++++++++++++++++- tests/util/test_json.py | 23 +++++------------ 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 68273c89743..1413f6d9b15 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -49,13 +49,6 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict: return {} if default is None else default -def _orjson_encoder(data: Any) -> str: - """JSON encoder that uses orjson.""" - return orjson.dumps( - data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS - ).decode("utf-8") - - def _orjson_default_encoder(data: Any) -> str: """JSON encoder that uses orjson with hass defaults.""" return orjson.dumps( @@ -79,21 +72,17 @@ def save_json( """ dump: Callable[[Any], Any] try: - if encoder: - # For backwards compatibility, if they pass in the - # default json encoder we use _orjson_default_encoder - # which is the orjson equivalent to the default encoder. - if encoder is DefaultHASSJSONEncoder: - dump = _orjson_default_encoder - json_data = _orjson_default_encoder(data) + # For backwards compatibility, if they pass in the + # default json encoder we use _orjson_default_encoder + # which is the orjson equivalent to the default encoder. + if encoder and encoder is not DefaultHASSJSONEncoder: # If they pass a custom encoder that is not the # DefaultHASSJSONEncoder, we use the slow path of json.dumps - else: - dump = json.dumps - json_data = json.dumps(data, indent=2, cls=encoder) + dump = json.dumps + json_data = json.dumps(data, indent=2, cls=encoder) else: - dump = _orjson_encoder - json_data = _orjson_encoder(data) + dump = _orjson_default_encoder + json_data = _orjson_default_encoder(data) except TypeError as error: msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data, dump=dump))}" _LOGGER.error(msg) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 53c1b8a4677..ca5cb92bfd5 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import json +from typing import NamedTuple from unittest.mock import Mock, patch import pytest @@ -13,8 +14,9 @@ from homeassistant.const import ( from homeassistant.core import CoreState from homeassistant.helpers import storage from homeassistant.util import dt +from homeassistant.util.color import RGBColor -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, async_test_home_assistant MOCK_VERSION = 1 MOCK_VERSION_2 = 2 @@ -460,3 +462,47 @@ async def test_changing_delayed_written_data(hass, store, hass_storage): "key": MOCK_KEY, "data": {"hello": "world"}, } + + +async def test_saving_load_round_trip(tmpdir): + """Test saving and loading round trip.""" + loop = asyncio.get_running_loop() + hass = await async_test_home_assistant(loop) + + hass.config.config_dir = await hass.async_add_executor_job( + tmpdir.mkdir, "temp_storage" + ) + + class NamedTupleSubclass(NamedTuple): + """A NamedTuple subclass.""" + + name: str + + nts = NamedTupleSubclass("a") + + data = { + "named_tuple_subclass": nts, + "rgb_color": RGBColor(255, 255, 0), + "set": {1, 2, 3}, + "list": [1, 2, 3], + "tuple": (1, 2, 3), + "dict_with_int": {1: 1, 2: 2}, + "dict_with_named_tuple": {1: nts, 2: nts}, + } + + store = storage.Store( + hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 + ) + await store.async_save(data) + load = await store.async_load() + assert load == { + "dict_with_int": {"1": 1, "2": 2}, + "dict_with_named_tuple": {"1": ["a"], "2": ["a"]}, + "list": [1, 2, 3], + "named_tuple_subclass": ["a"], + "rgb_color": [255, 255, 0], + "set": [1, 2, 3], + "tuple": [1, 2, 3], + } + + await hass.async_stop(force=True) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 28d321036c5..509c0376fae 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -12,7 +12,6 @@ import pytest from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder -from homeassistant.helpers.template import TupleWrapper from homeassistant.util.json import ( SerializationError, find_paths_unserializable_data, @@ -83,23 +82,15 @@ def test_overwrite_and_reload(atomic_writes): def test_save_bad_data(): """Test error from trying to save unserializable data.""" + + class CannotSerializeMe: + """Cannot serialize this.""" + with pytest.raises(SerializationError) as excinfo: - save_json("test4", {"hello": set()}) + save_json("test4", {"hello": CannotSerializeMe()}) - assert ( - "Failed to serialize to JSON: test4. Bad data at $.hello=set()(" - in str(excinfo.value) - ) - - -def test_save_bad_data_tuple_wrapper(): - """Test error from trying to save unserializable data.""" - with pytest.raises(SerializationError) as excinfo: - save_json("test4", {"hello": TupleWrapper(("4", "5"))}) - - assert ( - "Failed to serialize to JSON: test4. Bad data at $.hello=('4', '5')(" - in str(excinfo.value) + assert "Failed to serialize to JSON: test4. Bad data at $.hello=" in str( + excinfo.value ) From d8f3044ffa9cc7825b1493b28c260b1be434fb21 Mon Sep 17 00:00:00 2001 From: hahn-th Date: Sun, 17 Jul 2022 14:30:43 +0200 Subject: [PATCH 509/820] Bump homematicip 1.0.5 (#75334) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index f6fb9f3e739..db0833f8114 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.4"], + "requirements": ["homematicip==1.0.5"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 42360e81184..6a4b30192fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -837,7 +837,7 @@ home-assistant-frontend==20220707.1 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.4 +homematicip==1.0.5 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d6fa41d253..d82b023b4f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,7 +607,7 @@ home-assistant-frontend==20220707.1 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.4 +homematicip==1.0.5 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From cd223d91bb457d937e52de8384eda4e1b41749f1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 17 Jul 2022 15:01:26 +0200 Subject: [PATCH 510/820] Migrate Tractive to new entity naming style (#75184) --- homeassistant/components/tractive/binary_sensor.py | 5 +++-- homeassistant/components/tractive/device_tracker.py | 4 ++-- homeassistant/components/tractive/entity.py | 2 +- homeassistant/components/tractive/sensor.py | 9 +++++---- homeassistant/components/tractive/switch.py | 6 +++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index 001eb013a35..36aa2370a59 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -31,13 +31,14 @@ TRACKERS_WITH_BUILTIN_BATTERY = ("TRNJA4", "TRAXL1") class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): """Tractive sensor.""" + _attr_has_entity_name = True + def __init__( self, user_id: str, item: Trackables, description: BinarySensorEntityDescription ) -> None: """Initialize sensor entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{item.trackable['details']['name']} {description.name}" self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self.entity_description = description @@ -76,7 +77,7 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): SENSOR_TYPE = BinarySensorEntityDescription( key=ATTR_BATTERY_CHARGING, - name="Battery Charging", + name="Tracker battery charging", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, entity_category=EntityCategory.DIAGNOSTIC, ) diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 218151ae769..4b08defbf97 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -40,7 +40,9 @@ async def async_setup_entry( class TractiveDeviceTracker(TractiveEntity, TrackerEntity): """Tractive device tracker.""" + _attr_has_entity_name = True _attr_icon = "mdi:paw" + _attr_name = "Tracker" def __init__(self, user_id: str, item: Trackables) -> None: """Initialize tracker entity.""" @@ -51,8 +53,6 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): self._longitude: float = item.pos_report["latlong"][1] self._accuracy: int = item.pos_report["pos_uncertainty"] self._source_type: str = item.pos_report["sensor_used"] - - self._attr_name = f"{self._tracker_id} {item.trackable['details']['name']}" self._attr_unique_id = item.trackable["_id"] @property diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index fd29c6c6c6f..def321d928f 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -18,7 +18,7 @@ class TractiveEntity(Entity): self._attr_device_info = DeviceInfo( configuration_url="https://my.tractive.com/", identifiers={(DOMAIN, tracker_details["_id"])}, - name=f"Tractive ({tracker_details['_id']})", + name=trackable["details"]["name"], manufacturer="Tractive GmbH", sw_version=tracker_details["fw_version"], model=tracker_details["model_number"], diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index e19e10f6b44..c412502d8d9 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -48,6 +48,8 @@ class TractiveSensorEntityDescription( class TractiveSensor(TractiveEntity, SensorEntity): """Tractive sensor.""" + _attr_has_entity_name = True + def __init__( self, user_id: str, @@ -57,7 +59,6 @@ class TractiveSensor(TractiveEntity, SensorEntity): """Initialize sensor entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{item.trackable['details']['name']} {description.name}" self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self.entity_description = description @@ -133,7 +134,7 @@ class TractiveActivitySensor(TractiveSensor): SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_BATTERY_LEVEL, - name="Battery Level", + name="Tracker battery level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_class=TractiveHardwareSensor, @@ -149,14 +150,14 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( ), TractiveSensorEntityDescription( key=ATTR_MINUTES_ACTIVE, - name="Minutes Active", + name="Minutes active", icon="mdi:clock-time-eight-outline", native_unit_of_measurement=TIME_MINUTES, entity_class=TractiveActivitySensor, ), TractiveSensorEntityDescription( key=ATTR_DAILY_GOAL, - name="Daily Goal", + name="Daily goal", icon="mdi:flag-checkered", native_unit_of_measurement=TIME_MINUTES, entity_class=TractiveActivitySensor, diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index 2a425a8f2ac..87cf27b36b1 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -47,7 +47,7 @@ class TractiveSwitchEntityDescription( SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( TractiveSwitchEntityDescription( key=ATTR_BUZZER, - name="Tracker Buzzer", + name="Tracker buzzer", icon="mdi:volume-high", method="async_set_buzzer", entity_category=EntityCategory.CONFIG, @@ -61,7 +61,7 @@ SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( ), TractiveSwitchEntityDescription( key=ATTR_LIVE_TRACKING, - name="Live Tracking", + name="Live tracking", icon="mdi:map-marker-path", method="async_set_live_tracking", entity_category=EntityCategory.CONFIG, @@ -88,6 +88,7 @@ async def async_setup_entry( class TractiveSwitch(TractiveEntity, SwitchEntity): """Tractive switch.""" + _attr_has_entity_name = True entity_description: TractiveSwitchEntityDescription def __init__( @@ -99,7 +100,6 @@ class TractiveSwitch(TractiveEntity, SwitchEntity): """Initialize switch entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{item.trackable['details']['name']} {description.name}" self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self._attr_available = False self._tracker = item.tracker From 503b31fb1576f043759e84e8f519f4a52dce9df5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 17 Jul 2022 15:15:24 +0200 Subject: [PATCH 511/820] Migrate Xiaomi Miio to new entity naming style - part 1 (#75350) --- .../components/xiaomi_miio/binary_sensor.py | 22 +++---- .../components/xiaomi_miio/button.py | 9 ++- .../components/xiaomi_miio/device.py | 10 +-- homeassistant/components/xiaomi_miio/fan.py | 59 +++++++++--------- .../components/xiaomi_miio/humidifier.py | 12 ++-- .../components/xiaomi_miio/number.py | 21 +++---- .../components/xiaomi_miio/select.py | 9 ++- .../components/xiaomi_miio/sensor.py | 62 +++++++++---------- .../components/xiaomi_miio/switch.py | 19 +++--- .../components/xiaomi_miio/vacuum.py | 5 +- 10 files changed, 103 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index a1e833f42de..2b3dfde6190 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -57,13 +57,13 @@ class XiaomiMiioBinarySensorDescription(BinarySensorEntityDescription): BINARY_SENSOR_TYPES = ( XiaomiMiioBinarySensorDescription( key=ATTR_NO_WATER, - name="Water Tank Empty", + name="Water tank empty", icon="mdi:water-off-outline", entity_category=EntityCategory.DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_WATER_TANK_DETACHED, - name="Water Tank", + name="Water tank", icon="mdi:car-coolant-level", device_class=BinarySensorDeviceClass.CONNECTIVITY, value=lambda value: not value, @@ -71,13 +71,13 @@ BINARY_SENSOR_TYPES = ( ), XiaomiMiioBinarySensorDescription( key=ATTR_PTC_STATUS, - name="Auxiliary Heat Status", + name="Auxiliary heat status", device_class=BinarySensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_POWERSUPPLY_ATTACHED, - name="Power Supply", + name="Power supply", device_class=BinarySensorDeviceClass.PLUG, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -89,7 +89,7 @@ FAN_ZA5_BINARY_SENSORS = (ATTR_POWERSUPPLY_ATTACHED,) VACUUM_SENSORS = { ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_BOX_ATTACHED, - name="Mop Attached", + name="Mop attached", icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -98,7 +98,7 @@ VACUUM_SENSORS = { ), ATTR_WATER_BOX_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_BOX_ATTACHED, - name="Water Box Attached", + name="Water box attached", icon="mdi:water", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -107,7 +107,7 @@ VACUUM_SENSORS = { ), ATTR_WATER_SHORTAGE: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_SHORTAGE, - name="Water Shortage", + name="Water shortage", icon="mdi:water", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -120,7 +120,7 @@ VACUUM_SENSORS_SEPARATE_MOP = { **VACUUM_SENSORS, ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_MOP_ATTACHED, - name="Mop Attached", + name="Mop attached", icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -158,7 +158,6 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): continue entities.append( XiaomiGenericBinarySensor( - f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", @@ -199,7 +198,6 @@ async def async_setup_entry( continue entities.append( XiaomiGenericBinarySensor( - f"{config_entry.title} {description.name}", hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], config_entry, f"{description.key}_{config_entry.unique_id}", @@ -216,9 +214,9 @@ class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity) entity_description: XiaomiMiioBinarySensorDescription - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the entity.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self.entity_description = description self._attr_entity_registry_enabled_default = ( diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py index 0f5b59a262d..e02e6ad81bf 100644 --- a/homeassistant/components/xiaomi_miio/button.py +++ b/homeassistant/components/xiaomi_miio/button.py @@ -38,7 +38,7 @@ class XiaomiMiioButtonDescription(ButtonEntityDescription): BUTTON_TYPES = ( XiaomiMiioButtonDescription( key=ATTR_RESET_DUST_FILTER, - name="Reset Dust Filter", + name="Reset dust filter", icon="mdi:air-filter", method_press="reset_dust_filter", method_press_error_message="Resetting the dust filter lifetime failed", @@ -46,7 +46,7 @@ BUTTON_TYPES = ( ), XiaomiMiioButtonDescription( key=ATTR_RESET_UPPER_FILTER, - name="Reset Upper Filter", + name="Reset upper filter", icon="mdi:air-filter", method_press="reset_upper_filter", method_press_error_message="Resetting the upper filter lifetime failed.", @@ -86,7 +86,6 @@ async def async_setup_entry( entities.append( XiaomiGenericCoordinatedButton( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{unique_id}", @@ -105,9 +104,9 @@ class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.RESTART - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self.entity_description = description async def async_press(self) -> None: diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index f8b5e6bc45a..81ca71d6b68 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -110,7 +110,9 @@ class XiaomiMiioEntity(Entity): class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): """Representation of a base a coordinated Xiaomi Miio Entity.""" - def __init__(self, name, device, entry, unique_id, coordinator): + _attr_has_entity_name = True + + def __init__(self, device, entry, unique_id, coordinator): """Initialize the coordinated Xiaomi Miio Device.""" super().__init__(coordinator) self._device = device @@ -119,18 +121,12 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): self._device_id = entry.unique_id self._device_name = entry.title self._unique_id = unique_id - self._name = name @property def unique_id(self): """Return an unique ID.""" return self._unique_id - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name - @property def device_info(self) -> DeviceInfo: """Return the device info.""" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 8ce93933022..177f84679ee 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -191,7 +191,6 @@ async def async_setup_entry( hass.data.setdefault(DATA_KEY, {}) - name = config_entry.title model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -199,7 +198,6 @@ async def async_setup_entry( if model == MODEL_AIRPURIFIER_3C: entity = XiaomiAirPurifierMB4( - name, device, config_entry, unique_id, @@ -207,28 +205,27 @@ async def async_setup_entry( ) elif model in MODELS_PURIFIER_MIOT: entity = XiaomiAirPurifierMiot( - name, device, config_entry, unique_id, coordinator, ) elif model.startswith("zhimi.airpurifier."): - entity = XiaomiAirPurifier(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirPurifier(device, config_entry, unique_id, coordinator) elif model.startswith("zhimi.airfresh."): - entity = XiaomiAirFresh(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirFresh(device, config_entry, unique_id, coordinator) elif model == MODEL_AIRFRESH_A1: - entity = XiaomiAirFreshA1(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirFreshA1(device, config_entry, unique_id, coordinator) elif model == MODEL_AIRFRESH_T2017: - entity = XiaomiAirFreshT2017(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirFreshT2017(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_P5: - entity = XiaomiFanP5(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFanP5(device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIIO: - entity = XiaomiFan(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFan(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_ZA5: - entity = XiaomiFanZA5(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIOT: - entity = XiaomiFanMiot(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator) else: return @@ -277,9 +274,9 @@ async def async_setup_entry( class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._available_attributes = {} self._state = None @@ -349,9 +346,9 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): class XiaomiGenericAirPurifier(XiaomiGenericDevice): """Representation of a generic AirPurifier device.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic AirPurifier device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._speed_count = 100 @@ -396,9 +393,9 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO @@ -565,9 +562,9 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): """Representation of a Xiaomi Air Purifier MB4.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize Air Purifier MB4.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRPURIFIER_3C self._preset_modes = PRESET_MODES_AIRPURIFIER_3C @@ -619,9 +616,9 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): "Interval": AirfreshOperationMode.Interval, } - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH @@ -717,9 +714,9 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): class XiaomiAirFreshA1(XiaomiGenericAirPurifier): """Representation of a Xiaomi Air Fresh A1.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._favorite_speed = None self._device_features = FEATURE_FLAGS_AIRFRESH_A1 self._preset_modes = PRESET_MODES_AIRFRESH_A1 @@ -792,9 +789,9 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): class XiaomiAirFreshT2017(XiaomiAirFreshA1): """Representation of a Xiaomi Air Fresh T2017.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRFRESH_T2017 self._speed_range = (60, 300) @@ -802,9 +799,9 @@ class XiaomiAirFreshT2017(XiaomiAirFreshA1): class XiaomiGenericFan(XiaomiGenericDevice): """Representation of a generic Xiaomi Fan.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) if self._model == MODEL_FAN_P5: self._device_features = FEATURE_FLAGS_FAN_P5 @@ -877,9 +874,9 @@ class XiaomiGenericFan(XiaomiGenericDevice): class XiaomiFan(XiaomiGenericFan): """Representation of a Xiaomi Fan.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._state = self.coordinator.data.is_on self._oscillating = self.coordinator.data.oscillate @@ -968,9 +965,9 @@ class XiaomiFan(XiaomiGenericFan): class XiaomiFanP5(XiaomiGenericFan): """Representation of a Xiaomi Fan P5.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._state = self.coordinator.data.is_on self._preset_mode = self.coordinator.data.mode.name diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 05c1eaf35bf..0a9543ac604 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -73,12 +73,10 @@ async def async_setup_entry( model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - name = config_entry.title if model in MODELS_HUMIDIFIER_MIOT: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifierMiot( - name, air_humidifier, config_entry, unique_id, @@ -87,7 +85,6 @@ async def async_setup_entry( elif model in MODELS_HUMIDIFIER_MJJSQ: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifierMjjsq( - name, air_humidifier, config_entry, unique_id, @@ -96,7 +93,6 @@ async def async_setup_entry( else: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifier( - name, air_humidifier, config_entry, unique_id, @@ -115,9 +111,9 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): _attr_supported_features = HumidifierEntityFeature.MODES supported_features: int - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" - super().__init__(name, device, entry, unique_id, coordinator=coordinator) + super().__init__(device, entry, unique_id, coordinator=coordinator) self._state = None self._attributes = {} @@ -173,9 +169,9 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): available_modes: list[str] - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_min_humidity = 30 self._attr_max_humidity = 80 diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 7fd5347f432..e7c61044e25 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -107,7 +107,7 @@ class OscillationAngleValues: NUMBER_TYPES = { FEATURE_SET_MOTOR_SPEED: XiaomiMiioNumberDescription( key=ATTR_MOTOR_SPEED, - name="Motor Speed", + name="Motor speed", icon="mdi:fast-forward-outline", native_unit_of_measurement="rpm", native_min_value=200, @@ -119,7 +119,7 @@ NUMBER_TYPES = { ), FEATURE_SET_FAVORITE_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_LEVEL, - name="Favorite Level", + name="Favorite level", icon="mdi:star-cog", native_min_value=0, native_max_value=17, @@ -129,7 +129,7 @@ NUMBER_TYPES = { ), FEATURE_SET_FAN_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAN_LEVEL, - name="Fan Level", + name="Fan level", icon="mdi:fan", native_min_value=1, native_max_value=3, @@ -149,7 +149,7 @@ NUMBER_TYPES = { ), FEATURE_SET_OSCILLATION_ANGLE: XiaomiMiioNumberDescription( key=ATTR_OSCILLATION_ANGLE, - name="Oscillation Angle", + name="Oscillation angle", icon="mdi:angle-acute", native_unit_of_measurement=DEGREE, native_min_value=1, @@ -160,7 +160,7 @@ NUMBER_TYPES = { ), FEATURE_SET_DELAY_OFF_COUNTDOWN: XiaomiMiioNumberDescription( key=ATTR_DELAY_OFF_COUNTDOWN, - name="Delay Off Countdown", + name="Delay off countdown", icon="mdi:fan-off", native_unit_of_measurement=TIME_MINUTES, native_min_value=0, @@ -171,7 +171,7 @@ NUMBER_TYPES = { ), FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS, - name="Led Brightness", + name="LED brightness", icon="mdi:brightness-6", native_min_value=0, native_max_value=100, @@ -181,7 +181,7 @@ NUMBER_TYPES = { ), FEATURE_SET_LED_BRIGHTNESS_LEVEL: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS_LEVEL, - name="Led Brightness", + name="LED brightness", icon="mdi:brightness-6", native_min_value=0, native_max_value=8, @@ -191,7 +191,7 @@ NUMBER_TYPES = { ), FEATURE_SET_FAVORITE_RPM: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_RPM, - name="Favorite Motor Speed", + name="Favorite motor speed", icon="mdi:star-cog", native_unit_of_measurement="rpm", native_min_value=300, @@ -283,7 +283,6 @@ async def async_setup_entry( entities.append( XiaomiNumberEntity( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{config_entry.unique_id}", @@ -298,9 +297,9 @@ async def async_setup_entry( class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the generic Xiaomi attribute selector.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_native_value = self._extract_value_from_attribute( coordinator.data, description.key diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index f2fc736ed82..5f8fe8df591 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -110,7 +110,6 @@ async def async_setup_entry( description = SELECTOR_TYPES[FEATURE_SET_LED_BRIGHTNESS] entities.append( entity_class( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{config_entry.unique_id}", @@ -125,9 +124,9 @@ async def async_setup_entry( class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the generic Xiaomi attribute selector.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_options = list(description.options) self.entity_description = description @@ -135,9 +134,9 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): class XiaomiAirHumidifierSelector(XiaomiSelector): """Representation of a Xiaomi Air Humidifier selector.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator, description) + super().__init__(device, entry, unique_id, coordinator, description) self._current_led_brightness = self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 50feacf5da7..235d103b53d 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -172,13 +172,13 @@ SENSOR_TYPES = { ), ATTR_LOAD_POWER: XiaomiMiioSensorDescription( key=ATTR_LOAD_POWER, - name="Load Power", + name="Load power", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, ), ATTR_WATER_LEVEL: XiaomiMiioSensorDescription( key=ATTR_WATER_LEVEL, - name="Water Level", + name="Water level", native_unit_of_measurement=PERCENTAGE, icon="mdi:water-check", state_class=SensorStateClass.MEASUREMENT, @@ -186,7 +186,7 @@ SENSOR_TYPES = { ), ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( key=ATTR_ACTUAL_SPEED, - name="Actual Speed", + name="Actual speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -194,7 +194,7 @@ SENSOR_TYPES = { ), ATTR_CONTROL_SPEED: XiaomiMiioSensorDescription( key=ATTR_CONTROL_SPEED, - name="Control Speed", + name="Control speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -202,7 +202,7 @@ SENSOR_TYPES = { ), ATTR_FAVORITE_SPEED: XiaomiMiioSensorDescription( key=ATTR_FAVORITE_SPEED, - name="Favorite Speed", + name="Favorite speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -210,7 +210,7 @@ SENSOR_TYPES = { ), ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR_SPEED, - name="Motor Speed", + name="Motor speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -218,7 +218,7 @@ SENSOR_TYPES = { ), ATTR_MOTOR2_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR2_SPEED, - name="Second Motor Speed", + name="Second motor speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -226,7 +226,7 @@ SENSOR_TYPES = { ), ATTR_USE_TIME: XiaomiMiioSensorDescription( key=ATTR_USE_TIME, - name="Use Time", + name="Use time", native_unit_of_measurement=TIME_SECONDS, icon="mdi:progress-clock", state_class=SensorStateClass.TOTAL_INCREASING, @@ -269,7 +269,7 @@ SENSOR_TYPES = { ), ATTR_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_FILTER_LIFE_REMAINING, - name="Filter Life Remaining", + name="Filter life remaining", native_unit_of_measurement=PERCENTAGE, icon="mdi:air-filter", state_class=SensorStateClass.MEASUREMENT, @@ -278,7 +278,7 @@ SENSOR_TYPES = { ), ATTR_FILTER_USE: XiaomiMiioSensorDescription( key=ATTR_FILTER_HOURS_USED, - name="Filter Use", + name="Filter use", native_unit_of_measurement=TIME_HOURS, icon="mdi:clock-outline", state_class=SensorStateClass.MEASUREMENT, @@ -320,14 +320,14 @@ SENSOR_TYPES = { ), ATTR_CARBON_DIOXIDE: XiaomiMiioSensorDescription( key=ATTR_CARBON_DIOXIDE, - name="Carbon Dioxide", + name="Carbon dioxide", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), ATTR_PURIFY_VOLUME: XiaomiMiioSensorDescription( key=ATTR_PURIFY_VOLUME, - name="Purify Volume", + name="Purify volume", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, @@ -491,7 +491,7 @@ VACUUM_SENSORS = { f"dnd_{ATTR_DND_START}": XiaomiMiioSensorDescription( key=ATTR_DND_START, icon="mdi:minus-circle-off", - name="DnD Start", + name="DnD start", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, @@ -500,7 +500,7 @@ VACUUM_SENSORS = { f"dnd_{ATTR_DND_END}": XiaomiMiioSensorDescription( key=ATTR_DND_END, icon="mdi:minus-circle-off", - name="DnD End", + name="DnD end", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, @@ -509,7 +509,7 @@ VACUUM_SENSORS = { f"last_clean_{ATTR_LAST_CLEAN_START}": XiaomiMiioSensorDescription( key=ATTR_LAST_CLEAN_START, icon="mdi:clock-time-twelve", - name="Last Clean Start", + name="Last clean start", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, entity_category=EntityCategory.DIAGNOSTIC, @@ -519,7 +519,7 @@ VACUUM_SENSORS = { icon="mdi:clock-time-twelve", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - name="Last Clean End", + name="Last clean end", entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_TIME}": XiaomiMiioSensorDescription( @@ -527,7 +527,7 @@ VACUUM_SENSORS = { icon="mdi:timer-sand", key=ATTR_LAST_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - name="Last Clean Duration", + name="Last clean duration", entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( @@ -535,7 +535,7 @@ VACUUM_SENSORS = { icon="mdi:texture-box", key=ATTR_LAST_CLEAN_AREA, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - name="Last Clean Area", + name="Last clean area", entity_category=EntityCategory.DIAGNOSTIC, ), f"current_{ATTR_STATUS_CLEAN_TIME}": XiaomiMiioSensorDescription( @@ -543,7 +543,7 @@ VACUUM_SENSORS = { icon="mdi:timer-sand", key=ATTR_STATUS_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.status, - name="Current Clean Duration", + name="Current clean duration", entity_category=EntityCategory.DIAGNOSTIC, ), f"current_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( @@ -552,7 +552,7 @@ VACUUM_SENSORS = { key=ATTR_STATUS_CLEAN_AREA, parent_key=VacuumCoordinatorDataAttributes.status, entity_category=EntityCategory.DIAGNOSTIC, - name="Current Clean Area", + name="Current clean area", ), f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_DURATION}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -568,7 +568,7 @@ VACUUM_SENSORS = { icon="mdi:texture-box", key=ATTR_CLEAN_HISTORY_TOTAL_AREA, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, - name="Total Clean Area", + name="Total clean area", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -578,17 +578,17 @@ VACUUM_SENSORS = { state_class=SensorStateClass.TOTAL_INCREASING, key=ATTR_CLEAN_HISTORY_COUNT, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, - name="Total Clean Count", + name="Total clean count", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT}": XiaomiMiioSensorDescription( native_unit_of_measurement="", icon="mdi:counter", - state_class="total_increasing", + state_class=SensorStateClass.TOTAL_INCREASING, key=ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, - name="Total Dust Collection Count", + name="Total dust collection count", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -597,7 +597,7 @@ VACUUM_SENSORS = { icon="mdi:brush", key=ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Main Brush Left", + name="Main brush left", entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT}": XiaomiMiioSensorDescription( @@ -605,7 +605,7 @@ VACUUM_SENSORS = { icon="mdi:brush", key=ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Side Brush Left", + name="Side brush left", entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_FILTER_LEFT}": XiaomiMiioSensorDescription( @@ -613,7 +613,7 @@ VACUUM_SENSORS = { icon="mdi:air-filter", key=ATTR_CONSUMABLE_STATUS_FILTER_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Filter Left", + name="Filter left", entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT}": XiaomiMiioSensorDescription( @@ -621,7 +621,7 @@ VACUUM_SENSORS = { icon="mdi:eye-outline", key=ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Sensor Dirty Left", + name="Sensor dirty left", entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -644,7 +644,6 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): continue entities.append( XiaomiGenericSensor( - f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", @@ -741,7 +740,6 @@ async def async_setup_entry( continue entities.append( XiaomiGenericSensor( - f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", @@ -758,9 +756,9 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): entity_description: XiaomiMiioSensorDescription - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the entity.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self.entity_description = description self._attr_unique_id = unique_id self._attr_native_value = self._determine_native_value() diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index e3d34e8c512..f80b6343d09 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -231,7 +231,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_CHILD_LOCK, feature=FEATURE_SET_CHILD_LOCK, - name="Child Lock", + name="Child lock", icon="mdi:lock", method_on="async_set_child_lock_on", method_off="async_set_child_lock_off", @@ -249,7 +249,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_DRY, feature=FEATURE_SET_DRY, - name="Dry Mode", + name="Dry mode", icon="mdi:hair-dryer", method_on="async_set_dry_on", method_off="async_set_dry_off", @@ -258,7 +258,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_CLEAN, feature=FEATURE_SET_CLEAN, - name="Clean Mode", + name="Clean mode", icon="mdi:shimmer", method_on="async_set_clean_on", method_off="async_set_clean_off", @@ -268,7 +268,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_LED, feature=FEATURE_SET_LED, - name="Led", + name="LED", icon="mdi:led-outline", method_on="async_set_led_on", method_off="async_set_led_off", @@ -277,7 +277,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_LEARN_MODE, feature=FEATURE_SET_LEARN_MODE, - name="Learn Mode", + name="Learn mode", icon="mdi:school-outline", method_on="async_set_learn_mode_on", method_off="async_set_learn_mode_off", @@ -286,7 +286,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_AUTO_DETECT, feature=FEATURE_SET_AUTO_DETECT, - name="Auto Detect", + name="Auto detect", method_on="async_set_auto_detect_on", method_off="async_set_auto_detect_off", entity_category=EntityCategory.CONFIG, @@ -303,7 +303,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_PTC, feature=FEATURE_SET_PTC, - name="Auxiliary Heat", + name="Auxiliary heat", icon="mdi:radiator", method_on="async_set_ptc_on", method_off="async_set_ptc_off", @@ -353,7 +353,6 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): if description.feature & device_features: entities.append( XiaomiGenericCoordinatedSwitch( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{unique_id}", @@ -490,9 +489,9 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): entity_description: XiaomiMiioSwitchDescription - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_is_on = self._extract_value_from_attribute( self.coordinator.data, description.key diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 6d398ff40b9..e8f4b334544 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -86,11 +86,9 @@ async def async_setup_entry( entities = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: - name = config_entry.title unique_id = config_entry.unique_id mirobo = MiroboVacuum( - name, hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], config_entry, unique_id, @@ -201,14 +199,13 @@ class MiroboVacuum( def __init__( self, - name, device, entry, unique_id, coordinator: DataUpdateCoordinator[VacuumCoordinatorData], ): """Initialize the Xiaomi vacuum cleaner robot handler.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._state: str | None = None async def async_added_to_hass(self) -> None: From 8d63f81821e726a44a5aa1ebe6bb9283cd45b037 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 08:19:05 -0500 Subject: [PATCH 512/820] Add bluetooth discovery to HomeKit Controller (#75333) Co-authored-by: Jc2k --- .../homekit_controller/config_flow.py | 114 +++++++++-- .../homekit_controller/manifest.json | 3 +- .../homekit_controller/strings.json | 4 +- .../homekit_controller/translations/en.json | 4 +- homeassistant/generated/bluetooth.py | 5 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_config_flow.py | 189 +++++++++++++++--- 8 files changed, 268 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 9b8b759f80e..d8b3fda2d06 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,13 +1,17 @@ """Config flow to configure homekit_controller.""" from __future__ import annotations +from collections.abc import Awaitable import logging import re -from typing import Any +from typing import TYPE_CHECKING, Any, cast import aiohomekit -from aiohomekit.controller.abstract import AbstractPairing +from aiohomekit import Controller, const as aiohomekit_const +from aiohomekit.controller.abstract import AbstractDiscovery, AbstractPairing from aiohomekit.exceptions import AuthenticationError +from aiohomekit.model.categories import Categories +from aiohomekit.model.status_flags import StatusFlags from aiohomekit.utils import domain_supported, domain_to_name import voluptuous as vol @@ -16,6 +20,7 @@ from homeassistant.components import zeroconf from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.service_info import bluetooth from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES @@ -41,6 +46,8 @@ PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") _LOGGER = logging.getLogger(__name__) +BLE_DEFAULT_NAME = "Bluetooth device" + INSECURE_CODES = { "00000000", "11111111", @@ -62,6 +69,11 @@ def normalize_hkid(hkid: str) -> str: return hkid.lower() +def formatted_category(category: Categories) -> str: + """Return a human readable category name.""" + return str(category.name).replace("_", " ").title() + + @callback def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None: """Return a set of the configured hosts.""" @@ -92,14 +104,15 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the homekit_controller flow.""" - self.model = None - self.hkid = None - self.name = None - self.devices = {} - self.controller = None - self.finish_pairing = None + self.model: str | None = None + self.hkid: str | None = None + self.name: str | None = None + self.category: Categories | None = None + self.devices: dict[str, AbstractDiscovery] = {} + self.controller: Controller | None = None + self.finish_pairing: Awaitable[AbstractPairing] | None = None async def _async_setup_controller(self): """Create the controller.""" @@ -111,9 +124,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: key = user_input["device"] - self.hkid = self.devices[key].description.id - self.model = self.devices[key].description.model - self.name = self.devices[key].description.name + discovery = self.devices[key] + self.category = discovery.description.category + self.hkid = discovery.description.id + self.model = getattr(discovery.description, "model", BLE_DEFAULT_NAME) + self.name = discovery.description.name or BLE_DEFAULT_NAME await self.async_set_unique_id( normalize_hkid(self.hkid), raise_on_progress=False @@ -138,7 +153,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", errors=errors, data_schema=vol.Schema( - {vol.Required("device"): vol.In(self.devices.keys())} + { + vol.Required("device"): vol.In( + { + key: f"{key} ({formatted_category(discovery.description.category)})" + for key, discovery in self.devices.items() + } + ) + } ), ) @@ -151,13 +173,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self._async_setup_controller() try: - device = await self.controller.async_find(unique_id) + discovery = await self.controller.async_find(unique_id) except aiohomekit.AccessoryNotFoundError: return self.async_abort(reason="accessory_not_found_error") - self.name = device.description.name - self.model = device.description.model - self.hkid = device.description.id + self.name = discovery.description.name + self.model = discovery.description.model + self.category = discovery.description.category + self.hkid = discovery.description.id return self._async_step_pair_show_form() @@ -213,6 +236,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): model = properties["md"] name = domain_to_name(discovery_info.name) status_flags = int(properties["sf"]) + category = Categories(int(properties.get("ci", 0))) paired = not status_flags & 0x01 # The configuration number increases every time the characteristic map @@ -326,6 +350,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.name = name self.model = model + self.category = category self.hkid = hkid # We want to show the pairing form - but don't call async_step_pair @@ -333,6 +358,55 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # pairing code) return self._async_step_pair_show_form() + async def async_step_bluetooth( + self, discovery_info: bluetooth.BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED: + return self.async_abort(reason="ignored_model") + + # Late imports in case BLE is not available + from aiohomekit.controller.ble.discovery import ( # pylint: disable=import-outside-toplevel + BleDiscovery, + ) + from aiohomekit.controller.ble.manufacturer_data import ( # pylint: disable=import-outside-toplevel + HomeKitAdvertisement, + ) + + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + + mfr_data = discovery_info.manufacturer_data + + try: + device = HomeKitAdvertisement.from_manufacturer_data( + discovery_info.name, discovery_info.address, mfr_data + ) + except ValueError: + return self.async_abort(reason="ignored_model") + + if not (device.status_flags & StatusFlags.UNPAIRED): + return self.async_abort(reason="already_paired") + + if self.controller is None: + await self._async_setup_controller() + assert self.controller is not None + + try: + discovery = await self.controller.async_find(device.id) + except aiohomekit.AccessoryNotFoundError: + return self.async_abort(reason="accessory_not_found_error") + + if TYPE_CHECKING: + discovery = cast(BleDiscovery, discovery) + + self.name = discovery.description.name + self.model = BLE_DEFAULT_NAME + self.category = discovery.description.category + self.hkid = discovery.description.id + + return self._async_step_pair_show_form() + async def async_step_pair(self, pair_info=None): """Pair with a new HomeKit accessory.""" # If async_step_pair is called with no pairing code then we do the M1 @@ -453,8 +527,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_step_pair_show_form(self, errors=None): - placeholders = {"name": self.name} - self.context["title_placeholders"] = {"name": self.name} + placeholders = self.context["title_placeholders"] = { + "name": self.name, + "category": formatted_category(self.category), + } schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)} if errors and errors.get("pairing_code") == "insecure_setup_code": diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a5ca76fdc1e..a4f5350a167 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,8 +3,9 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.1"], + "requirements": ["aiohomekit==1.1.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], + "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_first_byte": 6 }], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 7ad868db3fc..2831dabc38d 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -1,7 +1,7 @@ { "title": "HomeKit Controller", "config": { - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "user": { "title": "Device selection", @@ -12,7 +12,7 @@ }, "pair": { "title": "Pair with a device via HomeKit Accessory Protocol", - "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", + "description": "HomeKit Controller communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", "data": { "pairing_code": "Pairing Code", "allow_insecure_setup_codes": "Allow pairing with insecure setup codes." diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 5de3a6c5334..2686e71d252 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -18,7 +18,7 @@ "unable_to_pair": "Unable to pair, please try again.", "unknown_error": "Device reported an unknown error. Pairing failed." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Abort pairing on all controllers, or try restarting the device, then continue to resume pairing.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Allow pairing with insecure setup codes.", "pairing_code": "Pairing Code" }, - "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", + "description": "HomeKit Controller communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", "title": "Pair with a device via HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 49596c4773c..feac27af1f1 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,11 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, str | int]] = [ + { + "domain": "homekit_controller", + "manufacturer_id": 76, + "manufacturer_data_first_byte": 6 + }, { "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" diff --git a/requirements_all.txt b/requirements_all.txt index 6a4b30192fe..47ff7005c66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.1 +aiohomekit==1.1.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d82b023b4f6..a9d015738f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.1 +aiohomekit==1.1.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 73bd159fd73..78d3c609a9c 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for homekit_controller config flow.""" import asyncio -from unittest import mock import unittest.mock from unittest.mock import AsyncMock, MagicMock, patch @@ -15,8 +14,13 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_FORM, + FlowResultType, +) from homeassistant.helpers import device_registry +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from tests.common import MockConfigEntry, mock_device_registry @@ -78,23 +82,55 @@ VALID_PAIRING_CODES = [ " 98765432 ", ] +NOT_HK_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="FakeAccessory", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + manufacturer_data={12: b"\x06\x12\x34"}, + service_data={}, + service_uuids=[], + source="local", +) -def _setup_flow_handler(hass, pairing=None): - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass - flow.context = {} +HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED = BluetoothServiceInfo( + name="Eve Energy Not Found", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + # ID is '9b:86:af:01:af:db' + manufacturer_data={ + 76: b"\x061\x01\x9b\x86\xaf\x01\xaf\xdb\x07\x00\x06\x00\x02\x02X\x19\xb1Q" + }, + service_data={}, + service_uuids=[], + source="local", +) - finish_pairing = unittest.mock.AsyncMock(return_value=pairing) +HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED = BluetoothServiceInfo( + name="Eve Energy Found Unpaired", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + # ID is '00:00:00:00:00:00', pairing flag is byte 3 + manufacturer_data={ + 76: b"\x061\x01\x00\x00\x00\x00\x00\x00\x07\x00\x06\x00\x02\x02X\x19\xb1Q" + }, + service_data={}, + service_uuids=[], + source="local", +) - discovery = mock.Mock() - discovery.description.id = "00:00:00:00:00:00" - discovery.async_start_pairing = unittest.mock.AsyncMock(return_value=finish_pairing) - flow.controller = mock.Mock() - flow.controller.pairings = {} - flow.controller.async_find = unittest.mock.AsyncMock(return_value=discovery) - - return flow +HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED = BluetoothServiceInfo( + name="Eve Energy Found Paired", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + # ID is '00:00:00:00:00:00', pairing flag is byte 3 + manufacturer_data={ + 76: b"\x061\x00\x00\x00\x00\x00\x00\x00\x07\x00\x06\x00\x02\x02X\x19\xb1Q" + }, + service_data={}, + service_uuids=[], + source="local", +) @pytest.mark.parametrize("pairing_code", INVALID_PAIRING_CODES) @@ -151,7 +187,7 @@ def get_device_discovery_info( "c#": device.description.config_num, "s#": device.description.state_num, "ff": "0", - "ci": "0", + "ci": "7", "sf": "0" if paired else "1", "sh": "", }, @@ -208,7 +244,7 @@ async def test_discovery_works(hass, controller, upper_case_props, missing_cshar assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_ZEROCONF, - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", } @@ -592,7 +628,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -607,7 +643,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -640,7 +676,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -653,7 +689,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected assert result["type"] == "form" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -680,7 +716,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -693,7 +729,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) assert result["type"] == "form" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -706,7 +742,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, "pairing": True, @@ -737,7 +773,7 @@ async def test_user_works(hass, controller): assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, "unique_id": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Other"}, } result = await hass.config_entries.flow.async_configure( @@ -772,7 +808,7 @@ async def test_user_pairing_with_insecure_setup_code(hass, controller): assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, "unique_id": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Other"}, } result = await hass.config_entries.flow.async_configure( @@ -829,7 +865,7 @@ async def test_unignore_works(hass, controller): assert result["type"] == "form" assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Other"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_UNIGNORE, } @@ -917,7 +953,7 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -942,7 +978,7 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): assert result["type"] == "form" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -967,3 +1003,98 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == {} + + +async def test_discovery_no_bluetooth_support(hass, controller): + """Test discovery with bluetooth support not available.""" + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + False, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ignored_model" + + +async def test_bluetooth_not_homekit(hass, controller): + """Test bluetooth discovery with a non-homekit device.""" + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_HK_BLUETOOTH_SERVICE_INFO, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ignored_model" + + +async def test_bluetooth_valid_device_no_discovery(hass, controller): + """Test bluetooth discovery with a homekit device and discovery fails.""" + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "accessory_not_found_error" + + +async def test_bluetooth_valid_device_discovery_paired(hass, controller): + """Test bluetooth discovery with a homekit device and discovery works.""" + setup_mock_accessory(controller) + + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_paired" + + +async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): + """Test bluetooth discovery with a homekit device and discovery works.""" + setup_mock_accessory(controller) + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pair" + + assert get_flow_context(hass, result) == { + "source": config_entries.SOURCE_BLUETOOTH, + "unique_id": "AA:BB:CC:DD:EE:FF", + "title_placeholders": {"name": "TestDevice", "category": "Other"}, + } + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2["type"] == RESULT_TYPE_FORM + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={"pairing_code": "111-22-333"} + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Koogeek-LS1-20833F" + assert result3["data"] == {} From 460f522d6dfff84540513dac46d1daaa4da6c861 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 17 Jul 2022 10:59:13 -0400 Subject: [PATCH 513/820] Migrate Deluge to new entity naming style (#75359) --- homeassistant/components/deluge/__init__.py | 2 ++ homeassistant/components/deluge/sensor.py | 5 ++--- homeassistant/components/deluge/switch.py | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index 2958b9e1df6..566b97b5b04 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -73,6 +73,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class DelugeEntity(CoordinatorEntity[DelugeDataUpdateCoordinator]): """Representation of a Deluge entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: """Initialize a Deluge entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 8a8e8f64657..bcdca8b3d92 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -52,14 +52,14 @@ SENSOR_TYPES: tuple[DelugeSensorEntityDescription, ...] = ( ), DelugeSensorEntityDescription( key=DOWNLOAD_SPEED, - name="Down Speed", + name="Down speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, value=lambda data: get_state(data, DOWNLOAD_SPEED), ), DelugeSensorEntityDescription( key=UPLOAD_SPEED, - name="Up Speed", + name="Up speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, value=lambda data: get_state(data, UPLOAD_SPEED), @@ -92,7 +92,6 @@ class DelugeSensor(DelugeEntity, SensorEntity): """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.config_entry.title} {description.name}" self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" @property diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index cc11fcaf86c..c25c1752ee4 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -29,7 +29,6 @@ class DelugeSwitch(DelugeEntity, SwitchEntity): def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: """Initialize the Deluge switch.""" super().__init__(coordinator) - self._attr_name = coordinator.config_entry.title self._attr_unique_id = f"{coordinator.config_entry.entry_id}_enabled" def turn_on(self, **kwargs: Any) -> None: From 27e3ff9c694abbcad74f1da1779539b04aa2da71 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 17 Jul 2022 11:16:14 -0400 Subject: [PATCH 514/820] Migrate Skybell to new entity naming style (#75341) --- homeassistant/components/skybell/camera.py | 2 +- homeassistant/components/skybell/entity.py | 5 ++--- homeassistant/components/skybell/light.py | 8 +------- homeassistant/components/skybell/sensor.py | 14 +++++++------- homeassistant/components/skybell/switch.py | 6 +++--- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 499f1f3bfca..5bbcea833c2 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) CAMERA_TYPES: tuple[CameraEntityDescription, ...] = ( - CameraEntityDescription(key="activity", name="Last Activity"), + CameraEntityDescription(key="activity", name="Last activity"), CameraEntityDescription(key="avatar", name="Camera"), ) diff --git a/homeassistant/components/skybell/entity.py b/homeassistant/components/skybell/entity.py index 0e5c246a8ed..29c7167b02b 100644 --- a/homeassistant/components/skybell/entity.py +++ b/homeassistant/components/skybell/entity.py @@ -16,6 +16,7 @@ class SkybellEntity(CoordinatorEntity[SkybellDataUpdateCoordinator]): """An HA implementation for Skybell entity.""" _attr_attribution = "Data provided by Skybell.com" + _attr_has_entity_name = True def __init__( self, coordinator: SkybellDataUpdateCoordinator, description: EntityDescription @@ -23,14 +24,12 @@ class SkybellEntity(CoordinatorEntity[SkybellDataUpdateCoordinator]): """Initialize a SkyBell entity.""" super().__init__(coordinator) self.entity_description = description - if description.name != coordinator.device.name: - self._attr_name = f"{self._device.name} {description.name}" self._attr_unique_id = f"{self._device.device_id}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._device.device_id)}, manufacturer=DEFAULT_NAME, model=self._device.type, - name=self._device.name, + name=self._device.name.capitalize(), sw_version=self._device.firmware_ver, ) if self._device.mac: diff --git a/homeassistant/components/skybell/light.py b/homeassistant/components/skybell/light.py index 845be44a34b..2b3066a8827 100644 --- a/homeassistant/components/skybell/light.py +++ b/homeassistant/components/skybell/light.py @@ -23,13 +23,7 @@ async def async_setup_entry( ) -> None: """Set up Skybell switch.""" async_add_entities( - SkybellLight( - coordinator, - LightEntityDescription( - key=coordinator.device.name, - name=coordinator.device.name, - ), - ) + SkybellLight(coordinator, LightEntityDescription(key="light")) for coordinator in hass.data[DOMAIN][entry.entry_id] ) diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index eeb81e07aaf..352d29bd793 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -35,27 +35,27 @@ class SkybellSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( SkybellSensorEntityDescription( key="chime_level", - name="Chime Level", + name="Chime level", icon="mdi:bell-ring", value_fn=lambda device: device.outdoor_chime_level, ), SkybellSensorEntityDescription( key="last_button_event", - name="Last Button Event", + name="Last button event", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.latest("button").get(CONST.CREATED_AT), ), SkybellSensorEntityDescription( key="last_motion_event", - name="Last Motion Event", + name="Last motion event", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.latest("motion").get(CONST.CREATED_AT), ), SkybellSensorEntityDescription( key=CONST.ATTR_LAST_CHECK_IN, - name="Last Check in", + name="Last check in", icon="mdi:clock", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, @@ -64,7 +64,7 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), SkybellSensorEntityDescription( key="motion_threshold", - name="Motion Threshold", + name="Motion threshold", icon="mdi:walk", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -72,7 +72,7 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), SkybellSensorEntityDescription( key="video_profile", - name="Video Profile", + name="Video profile", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.video_profile, @@ -87,7 +87,7 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), SkybellSensorEntityDescription( key=CONST.ATTR_WIFI_STATUS, - name="Wifi Status", + name="Wifi status", icon="mdi:wifi-strength-3", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index d4f2817141c..529be94f1ac 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -22,15 +22,15 @@ from .entity import SkybellEntity SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="do_not_disturb", - name="Do Not Disturb", + name="Do not disturb", ), SwitchEntityDescription( key="do_not_ring", - name="Do Not Ring", + name="Do not ring", ), SwitchEntityDescription( key="motion_sensor", - name="Motion Sensor", + name="Motion sensor", ), ) From 98dae902a13849518b6ee49ffc8bf1ee689e7c97 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 17 Jul 2022 17:45:38 +0200 Subject: [PATCH 515/820] Migrate PVOutput to new entity naming style (#75016) --- homeassistant/components/pvoutput/sensor.py | 9 ++-- tests/components/pvoutput/test_sensor.py | 53 +++++++++++++-------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 7bd4b8789eb..a0afee0f3eb 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -47,7 +47,7 @@ class PVOutputSensorEntityDescription( SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( PVOutputSensorEntityDescription( key="energy_consumption", - name="Energy Consumed", + name="Energy consumed", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -55,7 +55,7 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="energy_generation", - name="Energy Generated", + name="Energy generated", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -70,7 +70,7 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="power_consumption", - name="Power Consumed", + name="Power consumed", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -78,7 +78,7 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="power_generation", - name="Power Generated", + name="Power generated", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -129,6 +129,7 @@ class PVOutputSensorEntity( """Representation of a PVOutput sensor.""" entity_description: PVOutputSensorEntityDescription + _attr_has_entity_name = True def __init__( self, diff --git a/tests/components/pvoutput/test_sensor.py b/tests/components/pvoutput/test_sensor.py index a194326cd9d..54d1d3e641f 100644 --- a/tests/components/pvoutput/test_sensor.py +++ b/tests/components/pvoutput/test_sensor.py @@ -31,40 +31,46 @@ async def test_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.energy_consumed") - entry = entity_registry.async_get("sensor.energy_consumed") + state = hass.states.get("sensor.frenck_s_solar_farm_energy_consumed") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_consumed") assert entry assert state assert entry.unique_id == "12345_energy_consumption" assert entry.entity_category is None assert state.state == "1000" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Energy consumed" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.energy_generated") - entry = entity_registry.async_get("sensor.energy_generated") + state = hass.states.get("sensor.frenck_s_solar_farm_energy_generated") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_generated") assert entry assert state assert entry.unique_id == "12345_energy_generation" assert entry.entity_category is None assert state.state == "500" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Generated" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Energy generated" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.efficiency") - entry = entity_registry.async_get("sensor.efficiency") + state = hass.states.get("sensor.frenck_s_solar_farm_efficiency") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_efficiency") assert entry assert state assert entry.unique_id == "12345_normalized_output" assert entry.entity_category is None assert state.state == "0.5" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Efficiency" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Efficiency" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -73,54 +79,59 @@ async def test_sensors( assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.power_consumed") - entry = entity_registry.async_get("sensor.power_consumed") + state = hass.states.get("sensor.frenck_s_solar_farm_power_consumed") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_consumed") assert entry assert state assert entry.unique_id == "12345_power_consumption" assert entry.entity_category is None assert state.state == "2500" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Power consumed" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.power_generated") - entry = entity_registry.async_get("sensor.power_generated") + state = hass.states.get("sensor.frenck_s_solar_farm_power_generated") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_generated") assert entry assert state assert entry.unique_id == "12345_power_generation" assert entry.entity_category is None assert state.state == "1500" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Generated" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Power generated" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.temperature") - entry = entity_registry.async_get("sensor.temperature") + state = hass.states.get("sensor.frenck_s_solar_farm_temperature") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_temperature") assert entry assert state assert entry.unique_id == "12345_temperature" assert entry.entity_category is None assert state.state == "20.2" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Temperature" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Temperature" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.voltage") - entry = entity_registry.async_get("sensor.voltage") + state = hass.states.get("sensor.frenck_s_solar_farm_voltage") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_voltage") assert entry assert state assert entry.unique_id == "12345_voltage" assert entry.entity_category is None assert state.state == "220.5" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Voltage" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Voltage" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT assert ATTR_ICON not in state.attributes From 5beddb13c0239215ac4324ceb0a9e600aeb8efbc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 12:56:07 -0500 Subject: [PATCH 516/820] Fix HKC device triggers (#75371) --- .../components/homekit_controller/device_trigger.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index d7828753d98..700ab60c47f 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -17,7 +17,7 @@ from homeassistant.components.automation import ( from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS @@ -89,13 +89,13 @@ class TriggerSource: ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_data = automation_info["trigger_data"] + job = HassJob(action) + @callback def event_handler(char): if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: return - self._hass.async_create_task( - action({"trigger": {**trigger_data, **config}}) - ) + self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}}) trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] iid = trigger["characteristic"] @@ -236,11 +236,11 @@ async def async_setup_triggers_for_entry( def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]): """Process events generated by a HomeKit accessory into automation triggers.""" + trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS] for (aid, iid), ev in events.items(): if aid in conn.devices: device_id = conn.devices[aid] - if device_id in conn.hass.data[TRIGGERS]: - source = conn.hass.data[TRIGGERS][device_id] + if source := trigger_sources.get(device_id): source.fire(iid, ev) From 939c33b1dc1ab8a1c32fb39f718adc003fbace85 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 17 Jul 2022 13:31:14 -0700 Subject: [PATCH 517/820] Exclude calendar description from recorder (#75375) --- homeassistant/components/calendar/recorder.py | 10 +++++ tests/components/calendar/test_recorder.py | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 homeassistant/components/calendar/recorder.py create mode 100644 tests/components/calendar/test_recorder.py diff --git a/homeassistant/components/calendar/recorder.py b/homeassistant/components/calendar/recorder.py new file mode 100644 index 00000000000..4aba7b409cc --- /dev/null +++ b/homeassistant/components/calendar/recorder.py @@ -0,0 +1,10 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude potentially large attributes from being recorded in the database.""" + return {"description"} diff --git a/tests/components/calendar/test_recorder.py b/tests/components/calendar/test_recorder.py new file mode 100644 index 00000000000..0fbcaf38432 --- /dev/null +++ b/tests/components/calendar/test_recorder.py @@ -0,0 +1,44 @@ +"""The tests for calendar recorder.""" + +from datetime import timedelta + +from homeassistant.components.recorder.db_schema import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.recorder.common import async_wait_recording_done + + +async def test_events_http_api(hass, recorder_mock): + """Test the calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + + state = hass.states.get("calendar.calendar_1") + assert state + assert ATTR_FRIENDLY_NAME in state.attributes + assert "description" in state.attributes + + # calendar.calendar_1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_FRIENDLY_NAME in state.attributes + assert "description" not in state.attributes From a95c2c7850462478f7d2cbe1d5b36bb3c2cd7d90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 16:13:12 -0500 Subject: [PATCH 518/820] Avoid throwing on unsupported bleak client filter (#75378) * Avoid throwing on unsupported bleak client filter * Avoid throwing on unsupported bleak client filter --- homeassistant/components/bluetooth/models.py | 5 +++-- tests/components/bluetooth/test_init.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 43d4d0cb923..eda94305aa9 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -113,9 +113,10 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] """Map the filters.""" mapped_filters = {} if filters := kwargs.get("filters"): - if FILTER_UUIDS not in filters: + if filter_uuids := filters.get(FILTER_UUIDS): + mapped_filters[FILTER_UUIDS] = set(filter_uuids) + else: _LOGGER.warning("Only %s filters are supported", FILTER_UUIDS) - mapped_filters = {k: set(v) for k, v in filters.items()} if service_uuids := kwargs.get("service_uuids"): mapped_filters[FILTER_UUIDS] = set(service_uuids) if mapped_filters == self._mapped_filters: diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 1d348f42f0e..ec737cef593 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -686,6 +686,9 @@ async def test_wrapped_instance_unsupported_filter( assert models.HA_BLEAK_SCANNER is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( - filters={"unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + filters={ + "unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + "DuplicateData": True, + } ) assert "Only UUIDs filters are supported" in caplog.text From 91f2550bc3f8d0fc852d8e7a1fbf201a0279aebc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 17:25:45 -0500 Subject: [PATCH 519/820] Change manufacturer_data_first_byte to manufacturer_data_start (#75379) --- homeassistant/components/bluetooth/__init__.py | 18 ++++++++++-------- .../homekit_controller/manifest.json | 2 +- homeassistant/generated/bluetooth.py | 6 ++++-- homeassistant/loader.py | 2 +- script/hassfest/bluetooth.py | 2 +- script/hassfest/manifest.py | 2 +- tests/components/bluetooth/test_init.py | 8 +++++--- tests/test_loader.py | 4 ++-- 8 files changed, 25 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index b6058767669..ea058a72759 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -71,7 +71,7 @@ ADDRESS: Final = "address" LOCAL_NAME: Final = "local_name" SERVICE_UUID: Final = "service_uuid" MANUFACTURER_ID: Final = "manufacturer_id" -MANUFACTURER_DATA_FIRST_BYTE: Final = "manufacturer_data_first_byte" +MANUFACTURER_DATA_START: Final = "manufacturer_data_start" BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") @@ -157,14 +157,16 @@ def _ble_device_matches( return False if ( - matcher_manufacturer_data_first_byte := matcher.get( - MANUFACTURER_DATA_FIRST_BYTE + matcher_manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START) + ) is not None: + matcher_manufacturer_data_start_bytes = bytearray( + matcher_manufacturer_data_start ) - ) is not None and not any( - matcher_manufacturer_data_first_byte == manufacturer_data[0] - for manufacturer_data in advertisement_data.manufacturer_data.values() - ): - return False + if not any( + manufacturer_data.startswith(matcher_manufacturer_data_start_bytes) + for manufacturer_data in advertisement_data.manufacturer_data.values() + ): + return False return True diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a4f5350a167..17692476834 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": ["aiohomekit==1.1.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], - "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_first_byte": 6 }], + "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index feac27af1f1..03486b6043c 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -6,11 +6,13 @@ from __future__ import annotations # fmt: off -BLUETOOTH: list[dict[str, str | int]] = [ +BLUETOOTH: list[dict[str, str | int | list[int]]] = [ { "domain": "homekit_controller", "manufacturer_id": 76, - "manufacturer_data_first_byte": 6 + "manufacturer_data_start": [ + 6 + ] }, { "domain": "switchbot", diff --git a/homeassistant/loader.py b/homeassistant/loader.py index d0e6189ef96..9de06c48786 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -89,7 +89,7 @@ class BluetoothMatcherOptional(TypedDict, total=False): local_name: str service_uuid: str manufacturer_id: int - manufacturer_data_first_byte: int + manufacturer_data_start: list[int] class BluetoothMatcher(BluetoothMatcherRequired, BluetoothMatcherOptional): diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py index 77a8779efbd..d8277213f27 100644 --- a/script/hassfest/bluetooth.py +++ b/script/hassfest/bluetooth.py @@ -14,7 +14,7 @@ from __future__ import annotations # fmt: off -BLUETOOTH: list[dict[str, str | int]] = {} +BLUETOOTH: list[dict[str, str | int | list[int]]] = {} """.strip() diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 4f76fb9ed1e..9c1bd0a63f3 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -196,7 +196,7 @@ MANIFEST_SCHEMA = vol.Schema( vol.Optional("service_uuid"): vol.All(str, verify_lowercase), vol.Optional("local_name"): vol.All(str), vol.Optional("manufacturer_id"): int, - vol.Optional("manufacturer_data_first_byte"): int, + vol.Optional("manufacturer_data_start"): [int], } ) ], diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ec737cef593..a31f911f6a7 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -153,12 +153,12 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): async def test_discovery_match_by_manufacturer_id_and_first_byte( hass, mock_bleak_scanner_start ): - """Test bluetooth discovery match by manufacturer_id and manufacturer_data_first_byte.""" + """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start.""" mock_bt = [ { "domain": "homekit_controller", "manufacturer_id": 76, - "manufacturer_data_first_byte": 0x06, + "manufacturer_data_start": [0x06, 0x02, 0x03], } ] with patch( @@ -174,7 +174,9 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( hkc_device = BLEDevice("44:44:33:11:23:45", "lock") hkc_adv = AdvertisementData( - local_name="lock", service_uuids=[], manufacturer_data={76: b"\x06"} + local_name="lock", + service_uuids=[], + manufacturer_data={76: b"\x06\x02\x03\x99"}, ) models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) diff --git a/tests/test_loader.py b/tests/test_loader.py index b8a469f80aa..da788e0db75 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -205,7 +205,7 @@ def test_integration_properties(hass): {"hostname": "tesla_*", "macaddress": "98ED5C*"}, {"registered_devices": True}, ], - "bluetooth": [{"manufacturer_id": 76, "manufacturer_data_first_byte": 6}], + "bluetooth": [{"manufacturer_id": 76, "manufacturer_data_start": [0x06]}], "usb": [ {"vid": "10C4", "pid": "EA60"}, {"vid": "1CF1", "pid": "0030"}, @@ -244,7 +244,7 @@ def test_integration_properties(hass): {"vid": "10C4", "pid": "8A2A"}, ] assert integration.bluetooth == [ - {"manufacturer_id": 76, "manufacturer_data_first_byte": 6} + {"manufacturer_id": 76, "manufacturer_data_start": [0x06]} ] assert integration.ssdp == [ { From a8bb00f30581968eda231feda740790c5e64503a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 17:45:04 -0500 Subject: [PATCH 520/820] Fix availability in HKC for sleeping bluetooth devices (#75357) --- .../homekit_controller/connection.py | 79 +++++++++------- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_init.py | 89 ++++++++++++++++--- 5 files changed, 126 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 7e4dcc59be0..b2f3b3ae8e0 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable -import datetime +from datetime import timedelta import logging from types import MappingProxyType from typing import Any @@ -14,8 +14,8 @@ from aiohomekit.exceptions import ( AccessoryNotFoundError, EncryptionError, ) -from aiohomekit.model import Accessories, Accessory -from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes +from aiohomekit.model import Accessories, Accessory, Transport +from aiohomekit.model.characteristics import Characteristic from aiohomekit.model.services import Service from homeassistant.config_entries import ConfigEntry @@ -40,9 +40,9 @@ from .const import ( from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry from .storage import EntityMapStorage -DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 +BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds _LOGGER = logging.getLogger(__name__) @@ -122,6 +122,7 @@ class HKDevice: # If this is set polling is active and can be disabled by calling # this method. self._polling_interval_remover: CALLBACK_TYPE | None = None + self._ble_available_interval_remover: CALLBACK_TYPE | None = None # Never allow concurrent polling of the same accessory or bridge self._polling_lock = asyncio.Lock() @@ -133,9 +134,6 @@ class HKDevice: self.watchable_characteristics: list[tuple[int, int]] = [] - self.pairing.dispatcher_connect(self.process_new_events) - self.pairing.dispatcher_connect_config_changed(self.process_config_changed) - @property def entity_map(self) -> Accessories: """Return the accessories from the pairing.""" @@ -182,32 +180,15 @@ class HKDevice: self.available = available async_dispatcher_send(self.hass, self.signal_state_updated) - async def async_ensure_available(self) -> None: - """Verify the accessory is available after processing the entity map.""" - if self.available: - return - if self.watchable_characteristics and self.pollable_characteristics: - # We already tried, no need to try again - return - # We there are no watchable and not pollable characteristics, - # we need to force a connection to the device to verify its alive. - # - # This is similar to iOS's behavior for keeping alive connections - # to cameras. - # - primary = self.entity_map.accessories[0] - aid = primary.aid - iid = primary.accessory_information[CharacteristicsTypes.SERIAL_NUMBER].iid - await self.pairing.get_characteristics([(aid, iid)]) - self.async_set_available_state(True) - async def async_setup(self) -> None: """Prepare to use a paired HomeKit device in Home Assistant.""" entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] + pairing = self.pairing + transport = pairing.transport + entry = self.config_entry + if cache := entity_storage.get_map(self.unique_id): - self.pairing.restore_accessories_state( - cache["accessories"], cache["config_num"] - ) + pairing.restore_accessories_state(cache["accessories"], cache["config_num"]) # We need to force an update here to make sure we have # the latest values since the async_update we do in @@ -219,22 +200,47 @@ class HKDevice: # Ideally we would know which entities we are about to add # so we only poll those chars but that is not possible # yet. - await self.pairing.async_populate_accessories_state(force_update=True) + try: + await self.pairing.async_populate_accessories_state(force_update=True) + except AccessoryNotFoundError: + if transport != Transport.BLE or not cache: + # BLE devices may sleep and we can't force a connection + raise + + entry.async_on_unload(pairing.dispatcher_connect(self.process_new_events)) + entry.async_on_unload( + pairing.dispatcher_connect_config_changed(self.process_config_changed) + ) + entry.async_on_unload( + pairing.dispatcher_availability_changed(self.async_set_available_state) + ) await self.async_process_entity_map() - await self.async_ensure_available() - if not cache: # If its missing from the cache, make sure we save it self.async_save_entity_map() # If everything is up to date, we can create the entities # since we know the data is not stale. await self.async_add_new_entities() + + self.async_set_available_state(self.pairing.is_available) + self._polling_interval_remover = async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL + self.hass, self.async_update, self.pairing.poll_interval ) + if transport == Transport.BLE: + # If we are using BLE, we need to periodically check of the + # BLE device is available since we won't get callbacks + # when it goes away since we HomeKit supports disconnected + # notifications and we cannot treat a disconnect as unavailability. + self._ble_available_interval_remover = async_track_time_interval( + self.hass, + self.async_update_available_state, + timedelta(seconds=BLE_AVAILABILITY_CHECK_INTERVAL), + ) + async def async_add_new_entities(self) -> None: """Add new entities to Home Assistant.""" await self.async_load_platforms() @@ -546,10 +552,15 @@ class HKDevice: if tasks: await asyncio.gather(*tasks) + @callback + def async_update_available_state(self, *_: Any) -> None: + """Update the available state of the device.""" + self.async_set_available_state(self.pairing.is_available) + async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" if not self.pollable_characteristics: - self.async_set_available_state(self.pairing.is_connected) + self.async_update_available_state() _LOGGER.debug( "HomeKit connection not polling any characteristics: %s", self.unique_id ) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 17692476834..29268f3b6e9 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.4"], + "requirements": ["aiohomekit==1.1.5"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 47ff7005c66..a8d87c8a557 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.4 +aiohomekit==1.1.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9d015738f5..ebfaabd16c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.4 +aiohomekit==1.1.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index df14ad325b6..37d41fcf372 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -3,15 +3,15 @@ from datetime import timedelta from unittest.mock import patch -from aiohomekit import AccessoryDisconnectedError -from aiohomekit.model import Accessory +from aiohomekit import AccessoryNotFoundError +from aiohomekit.model import Accessory, Transport from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from aiohomekit.testing import FakePairing from homeassistant.components.homekit_controller.const import DOMAIN, ENTITY_MAP from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry @@ -104,31 +104,35 @@ async def test_offline_device_raises(hass, controller): is_connected = False class OfflineFakePairing(FakePairing): - """Fake pairing that always returns False for is_connected.""" + """Fake pairing that can flip is_connected.""" @property def is_connected(self): nonlocal is_connected return is_connected + @property + def is_available(self): + return self.is_connected + async def async_populate_accessories_state(self, *args, **kwargs): nonlocal is_connected if not is_connected: - raise AccessoryDisconnectedError("any") + raise AccessoryNotFoundError("any") async def get_characteristics(self, chars, *args, **kwargs): nonlocal is_connected if not is_connected: - raise AccessoryDisconnectedError("any") + raise AccessoryNotFoundError("any") return {} + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) + with patch("aiohomekit.testing.FakePairing", OfflineFakePairing): await async_setup_component(hass, DOMAIN, {}) - accessory = Accessory.create_with_info( - "TestDevice", "example.com", "Test", "0001", "0.1" - ) - create_alive_service(accessory) - config_entry, _ = await setup_test_accessories_with_controller( hass, [accessory], controller ) @@ -141,3 +145,66 @@ async def test_offline_device_raises(hass, controller): async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.LOADED + assert hass.states.get("light.testdevice").state == STATE_OFF + + +async def test_ble_device_only_checks_is_available(hass, controller): + """Test a BLE device only checks is_available.""" + + is_available = False + + class FakeBLEPairing(FakePairing): + """Fake BLE pairing that can flip is_available.""" + + @property + def transport(self): + return Transport.BLE + + @property + def is_connected(self): + return False + + @property + def is_available(self): + nonlocal is_available + return is_available + + async def async_populate_accessories_state(self, *args, **kwargs): + nonlocal is_available + if not is_available: + raise AccessoryNotFoundError("any") + + async def get_characteristics(self, chars, *args, **kwargs): + nonlocal is_available + if not is_available: + raise AccessoryNotFoundError("any") + return {} + + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) + + with patch("aiohomekit.testing.FakePairing", FakeBLEPairing): + await async_setup_component(hass, DOMAIN, {}) + config_entry, _ = await setup_test_accessories_with_controller( + hass, [accessory], controller + ) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + is_available = True + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert hass.states.get("light.testdevice").state == STATE_OFF + + is_available = False + async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + assert hass.states.get("light.testdevice").state == STATE_UNAVAILABLE + + is_available = True + async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + assert hass.states.get("light.testdevice").state == STATE_OFF From 4d5673013b7a9e1e7f58506f960cfa60e428db85 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 18 Jul 2022 00:24:34 +0000 Subject: [PATCH 521/820] [ci skip] Translation update --- .../components/generic/translations/tr.json | 4 +++ .../here_travel_time/translations/tr.json | 7 +++++ .../homeassistant/translations/tr.json | 1 + .../homekit_controller/translations/ca.json | 4 +-- .../homekit_controller/translations/et.json | 4 +-- .../translations/zh-Hant.json | 4 +-- .../components/nextdns/translations/tr.json | 29 +++++++++++++++++++ .../components/rhasspy/translations/tr.json | 12 ++++++++ .../components/verisure/translations/tr.json | 15 +++++++++- .../components/withings/translations/tr.json | 4 +++ 10 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/nextdns/translations/tr.json create mode 100644 homeassistant/components/rhasspy/translations/tr.json diff --git a/homeassistant/components/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json index d439c559aa5..efce9d014b1 100644 --- a/homeassistant/components/generic/translations/tr.json +++ b/homeassistant/components/generic/translations/tr.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Bu URL ayarlar\u0131na sahip bir kamera zaten var.", "invalid_still_image": "URL ge\u00e7erli bir hareketsiz resim d\u00f6nd\u00fcrmedi", + "malformed_url": "Hatal\u0131 bi\u00e7imlendirilmi\u015f URL", "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", + "relative_url": "G\u00f6receli URL'lere izin verilmez", "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Bu URL ayarlar\u0131na sahip bir kamera zaten var.", "invalid_still_image": "URL ge\u00e7erli bir hareketsiz resim d\u00f6nd\u00fcrmedi", + "malformed_url": "Hatal\u0131 bi\u00e7imlendirilmi\u015f URL", "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", + "relative_url": "G\u00f6receli URL'lere izin verilmez", "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", diff --git a/homeassistant/components/here_travel_time/translations/tr.json b/homeassistant/components/here_travel_time/translations/tr.json index 181588ba54a..10be792e0ee 100644 --- a/homeassistant/components/here_travel_time/translations/tr.json +++ b/homeassistant/components/here_travel_time/translations/tr.json @@ -39,6 +39,13 @@ }, "title": "Kalk\u0131\u015f Se\u00e7in" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Bir harita konumu kullan\u0131n", + "origin_entity": "Bir varl\u0131\u011f\u0131 kullan\u0131n" + }, + "title": "Kalk\u0131\u015f Se\u00e7in" + }, "user": { "data": { "api_key": "API Anahtar\u0131", diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index cffbee33e74..7ec8074702b 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU Mimarisi", + "config_dir": "Yap\u0131land\u0131rma Dizini", "dev": "Geli\u015ftirme", "docker": "Docker", "hassio": "S\u00fcperviz\u00f6r", diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index ff9f180c943..5cb98f7287d 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -18,7 +18,7 @@ "unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.", "unknown_error": "El dispositiu ha em\u00e8s un error desconegut. Vinculaci\u00f3 fallida." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Atura la vinculaci\u00f3 a tots els controladors o prova de reiniciar el dispositiu, despr\u00e9s, segueix amb la vinculaci\u00f3.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Permet la vinculaci\u00f3 amb codis de configuraci\u00f3 insegurs.", "pairing_code": "Codi de vinculaci\u00f3" }, - "description": "El controlador HomeKit es comunica amb {name} a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura encriptada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el propi dispositiu o en la seva caixa.", + "description": "El controlador HomeKit es comunica amb {name} ({category}) a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura xifrada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el mateix dispositiu o en la seva caixa.", "title": "Vinculaci\u00f3 amb un dispositiu a trav\u00e9s de HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json index c658213eea0..8db1df29803 100644 --- a/homeassistant/components/homekit_controller/translations/et.json +++ b/homeassistant/components/homekit_controller/translations/et.json @@ -18,7 +18,7 @@ "unable_to_pair": "Ei saa siduda, proovi uuesti.", "unknown_error": "Seade teatas tundmatust t\u00f5rkest. Sidumine nurjus." }, - "flow_title": "{name}", + "flow_title": "{name} ( {category} )", "step": { "busy_error": { "description": "Katkesta sidumine k\u00f5igis kontrollerites v\u00f5i proovi seade taask\u00e4ivitada ja j\u00e4tka sidumist.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Luba sidumist ebaturvalise salas\u00f5naga.", "pairing_code": "Sidumiskood" }, - "description": "HomeKiti kontroller suhtleb seadmega {name} kohtv\u00f5rgu kaudu, kasutades turvalist kr\u00fcpteeritud \u00fchendust ilma eraldi HomeKiti kontrolleri v\u00f5i iCloudita. Selle lisaseadme kasutamiseks sisesta oma HomeKiti sidumiskood (vormingus XXX-XX-XXX). See kood on tavaliselt seadmel v\u00f5i pakendil.", + "description": "HomeKiti kontroller suhtleb seadmega {name}({category}) kohtv\u00f5rgu kaudu, kasutades turvalist kr\u00fcpteeritud \u00fchendust ilma eraldi HomeKiti kontrolleri v\u00f5i iCloudita. Selle lisaseadme kasutamiseks sisesta oma HomeKiti sidumiskood (vormingus XXX-XX-XXX). See kood on tavaliselt seadmel v\u00f5i pakendil.", "title": "Seadme sidumine HomeKiti tarvikuprotokolli kaudu" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index e13b69bd0fd..63b4b45f520 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -18,7 +18,7 @@ "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "unknown_error": "\u88dd\u7f6e\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "\u53d6\u6d88\u6240\u6709\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u6216\u8005\u91cd\u555f\u88dd\u7f6e\u3001\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "\u5141\u8a31\u8207\u4e0d\u5b89\u5168\u8a2d\u5b9a\u4ee3\u78bc\u9032\u884c\u914d\u5c0d\u3002", "pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc" }, - "description": "\u4f7f\u7528 {name} \u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u88dd\u7f6e\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", + "description": "\u4f7f\u7528 {name} ({category}) \u4e4b HomeKit \u63a7\u5236\u5668\u672c\u5730\u7aef\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u88dd\u7f6e\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", "title": "\u900f\u904e HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a\u6240\u914d\u5c0d\u88dd\u7f6e" }, "protocol_error": { diff --git a/homeassistant/components/nextdns/translations/tr.json b/homeassistant/components/nextdns/translations/tr.json new file mode 100644 index 00000000000..5cf9dc9b2c2 --- /dev/null +++ b/homeassistant/components/nextdns/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Bu NextDNS profili zaten yap\u0131land\u0131r\u0131lm\u0131\u015f." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Eri\u015fim sunucusu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/tr.json b/homeassistant/components/rhasspy/translations/tr.json new file mode 100644 index 00000000000..a8f9bf1204c --- /dev/null +++ b/homeassistant/components/rhasspy/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Rhasspy deste\u011fini etkinle\u015ftirmek istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/tr.json b/homeassistant/components/verisure/translations/tr.json index 88a0d943872..e339b3d6997 100644 --- a/homeassistant/components/verisure/translations/tr.json +++ b/homeassistant/components/verisure/translations/tr.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "unknown_mfa": "MFA kurulumu s\u0131ras\u0131nda bilinmeyen bir hata olu\u015ftu" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant, Sayfalar\u0131m hesab\u0131n\u0131zda birden fazla Verisure y\u00fcklemesi buldu. L\u00fctfen Home Assistant'a eklemek i\u00e7in kurulumu se\u00e7in." }, + "mfa": { + "data": { + "code": "Do\u011frulama kodu", + "description": "Hesab\u0131n\u0131zda 2 ad\u0131ml\u0131 do\u011frulama etkinle\u015ftirildi. L\u00fctfen Verisure'un size g\u00f6nderdi\u011fi do\u011frulama kodunu girin." + } + }, "reauth_confirm": { "data": { "description": "Verisure My Pages hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n.", @@ -22,6 +29,12 @@ "password": "Parola" } }, + "reauth_mfa": { + "data": { + "code": "Do\u011frulama kodu", + "description": "Hesab\u0131n\u0131zda 2 ad\u0131ml\u0131 do\u011frulama etkinle\u015ftirildi. L\u00fctfen Verisure'un size g\u00f6nderdi\u011fi do\u011frulama kodunu girin." + } + }, "user": { "data": { "description": "Verisure My Pages hesab\u0131n\u0131zla oturum a\u00e7\u0131n.", diff --git a/homeassistant/components/withings/translations/tr.json b/homeassistant/components/withings/translations/tr.json index 31b2a424071..698fe41988c 100644 --- a/homeassistant/components/withings/translations/tr.json +++ b/homeassistant/components/withings/translations/tr.json @@ -27,6 +27,10 @@ "reauth": { "description": "Withings verilerini almaya devam etmek i\u00e7in \" {profile}", "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "reauth_confirm": { + "description": "Withings verilerini almaya devam etmek i\u00e7in \" {profile} \" profilinin yeniden do\u011frulanmas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" } } } From e522b6e3b898767a383f3918cf9afc4518fe810d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 01:36:01 -0500 Subject: [PATCH 522/820] Bump aiohomekit to 1.1.7 (#75384) BLEDevice is now passed around instead of the address internally to avoid bleak creating a new scanner each time a client needs to resolve the address to os details --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 29268f3b6e9..192a5cb701a 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.5"], + "requirements": ["aiohomekit==1.1.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index a8d87c8a557..1f549530318 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.5 +aiohomekit==1.1.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ebfaabd16c3..ba7307a7cc3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.5 +aiohomekit==1.1.7 # homeassistant.components.emulated_hue # homeassistant.components.http From 943e0b9cf7b9b99cf2e9c156da068e0ff4fd140f Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 18 Jul 2022 17:40:18 +0800 Subject: [PATCH 523/820] Yolink feature garage door (#75120) --- .coveragerc | 1 + homeassistant/components/yolink/__init__.py | 1 + .../components/yolink/binary_sensor.py | 9 +- homeassistant/components/yolink/cover.py | 86 +++++++++++++++++++ homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/yolink/cover.py diff --git a/.coveragerc b/.coveragerc index 0a2e8e57fa8..0fb2210b3cd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1498,6 +1498,7 @@ omit = homeassistant/components/yolink/climate.py homeassistant/components/yolink/const.py homeassistant/components/yolink/coordinator.py + homeassistant/components/yolink/cover.py homeassistant/components/yolink/entity.py homeassistant/components/yolink/lock.py homeassistant/components/yolink/sensor.py diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 714b0b25070..3257d64d265 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -27,6 +27,7 @@ SCAN_INTERVAL = timedelta(minutes=5) PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.COVER, Platform.LOCK, Platform.SENSOR, Platform.SIREN, diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index b296e01fa56..6e29bccf437 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -47,6 +47,13 @@ SENSOR_DEVICE_TYPE = [ ] +def is_door_sensor(device: YoLinkDevice) -> bool: + """Check Door Sensor type.""" + return device.device_type == ATTR_DEVICE_DOOR_SENSOR and ( + device.parent_id is None or device.parent_id == "null" + ) + + SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( YoLinkBinarySensorEntityDescription( key="door_state", @@ -54,7 +61,7 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.DOOR, name="State", value=lambda value: value == "open" if value is not None else None, - exists_fn=lambda device: device.device_type == ATTR_DEVICE_DOOR_SENSOR, + exists_fn=is_door_sensor, ), YoLinkBinarySensorEntityDescription( key="motion_state", diff --git a/homeassistant/components/yolink/cover.py b/homeassistant/components/yolink/cover.py new file mode 100644 index 00000000000..e7e32764ca8 --- /dev/null +++ b/homeassistant/components/yolink/cover.py @@ -0,0 +1,86 @@ +"""YoLink Garage Door.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import ( + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_COORDINATORS, ATTR_DEVICE_DOOR_SENSOR, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink garage door from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + entities = [ + YoLinkCoverEntity(config_entry, device_coordinator) + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type == ATTR_DEVICE_DOOR_SENSOR + and device_coordinator.device.parent_id is not None + and device_coordinator.device.parent_id != "null" + ] + async_add_entities(entities) + + +class YoLinkCoverEntity(YoLinkEntity, CoverEntity): + """YoLink Cover Entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + ) -> None: + """Init YoLink garage door entity.""" + super().__init__(config_entry, coordinator) + self._attr_unique_id = f"{coordinator.device.device_id}_door_state" + self._attr_name = f"{coordinator.device.device_name} (State)" + self._attr_device_class = CoverDeviceClass.GARAGE + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + + @callback + def update_entity_state(self, state: dict[str, Any]) -> None: + """Update HA Entity State.""" + self._attr_is_closed = state.get("state") == "closed" + self.async_write_ha_state() + + async def toggle_garage_state(self, state: str) -> None: + """Toggle Garage door state.""" + # make sure current state is correct + await self.coordinator.async_refresh() + if state == "open" and self.is_closed is False: + return + if state == "close" and self.is_closed is True: + return + # get paired controller + door_controller_coordinator = self.hass.data[DOMAIN][ + self.config_entry.entry_id + ][ATTR_COORDINATORS].get(self.coordinator.device.parent_id) + if door_controller_coordinator is None: + raise ValueError( + "This device has not been paired with a garage door controller" + ) + # call controller api open/close garage door + await door_controller_coordinator.device.call_device_http_api("toggle", None) + await self.coordinator.async_refresh() + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open garage door.""" + await self.toggle_garage_state("open") + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close garage door.""" + await self.toggle_garage_state("close") diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index d2a02a44c42..0db736938f7 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.8"], + "requirements": ["yolink-api==0.0.9"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1f549530318..ad2c15cf77b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.8 +yolink-api==0.0.9 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba7307a7cc3..784b740d459 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ yalexs==1.1.25 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.8 +yolink-api==0.0.9 # homeassistant.components.youless youless-api==0.16 From ca5065a62734fef757ad05f55caa012aeb5e9bde Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 18 Jul 2022 02:44:51 -0700 Subject: [PATCH 524/820] Improve google calendar config flow timeout error messages (#75364) --- .../components/google/config_flow.py | 3 +++ homeassistant/components/google/strings.json | 1 + tests/components/google/test_config_flow.py | 21 ++++++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 22b62094e76..e320212ca1b 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -91,6 +91,9 @@ class OAuth2FlowHandler( self.flow_impl.client_secret, calendar_access, ) + except TimeoutError as err: + _LOGGER.error("Timeout initializing device flow: %s", str(err)) + return self.async_abort(reason="timeout_connect") except OAuthError as err: _LOGGER.error("Error initializing device flow: %s", str(err)) return self.async_abort(reason="oauth_error") diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 3ff75047f70..b4c5270e003 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -16,6 +16,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "code_expired": "Authentication code expired or credential setup is invalid, please try again.", diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 24ad8a7b769..5c373fb2219 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -242,7 +242,7 @@ async def test_code_error( mock_code_flow: Mock, component_setup: ComponentSetup, ) -> None: - """Test successful creds setup.""" + """Test server error setting up the oauth flow.""" assert await component_setup() with patch( @@ -256,6 +256,25 @@ async def test_code_error( assert result.get("reason") == "oauth_error" +async def test_timeout_error( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, +) -> None: + """Test timeout error setting up the oauth flow.""" + assert await component_setup() + + with patch( + "homeassistant.components.google.api.OAuth2WebServerFlow.step1_get_device_and_user_codes", + side_effect=TimeoutError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "timeout_connect" + + @pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(seconds=50)]) async def test_expired_after_exchange( hass: HomeAssistant, From 6fdb414b58ec11a809d7c2ce9566797824faa62c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 18 Jul 2022 06:12:13 -0400 Subject: [PATCH 525/820] Migrate Goalzero to new entity naming style (#75358) --- .../components/goalzero/binary_sensor.py | 4 ++-- homeassistant/components/goalzero/entity.py | 6 ++--- homeassistant/components/goalzero/sensor.py | 22 +++++++++---------- homeassistant/components/goalzero/switch.py | 6 ++--- tests/components/goalzero/test_sensor.py | 2 +- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index c4219b51e6c..db8a1b788f2 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -26,7 +26,7 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( ), BinarySensorEntityDescription( key="app_online", - name="App Online", + name="App online", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -37,7 +37,7 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( ), BinarySensorEntityDescription( key="inputDetected", - name="Input Detected", + name="Input detected", device_class=BinarySensorDeviceClass.POWER, ), ) diff --git a/homeassistant/components/goalzero/entity.py b/homeassistant/components/goalzero/entity.py index 8c696ce1377..eef6ea43d9c 100644 --- a/homeassistant/components/goalzero/entity.py +++ b/homeassistant/components/goalzero/entity.py @@ -15,6 +15,7 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): """Representation of a Goal Zero Yeti entity.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -24,9 +25,6 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): """Initialize a Goal Zero Yeti entity.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = ( - f"{coordinator.config_entry.data[CONF_NAME]} {description.name}" - ) self._attr_unique_id = f"{coordinator.config_entry.entry_id}/{description.key}" @property @@ -37,7 +35,7 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, manufacturer=MANUFACTURER, model=self._api.sysdata[ATTR_MODEL], - name=self.coordinator.config_entry.data[CONF_NAME], + name=self.coordinator.config_entry.data[CONF_NAME].capitalize(), sw_version=self._api.data["firmwareVersion"], ) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index ef95578820d..345c3b41f7d 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -32,14 +32,14 @@ from .entity import GoalZeroEntity SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wattsIn", - name="Watts In", + name="Watts in", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ampsIn", - name="Amps In", + name="Amps in", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -47,14 +47,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wattsOut", - name="Watts Out", + name="Watts out", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ampsOut", - name="Amps Out", + name="Amps out", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -62,7 +62,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="whOut", - name="WH Out", + name="Wh out", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, @@ -70,7 +70,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="whStored", - name="WH Stored", + name="Wh stored", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=SensorStateClass.MEASUREMENT, @@ -84,13 +84,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="socPercent", - name="State of Charge Percent", + name="State of charge percent", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="timeToEmptyFull", - name="Time to Empty/Full", + name="Time to empty/full", device_class=TIME_MINUTES, native_unit_of_measurement=TIME_MINUTES, ), @@ -103,7 +103,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wifiStrength", - name="Wifi Strength", + name="Wi-Fi strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, entity_registry_enabled_default=False, @@ -111,7 +111,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="timestamp", - name="Total Run Time", + name="Total run time", native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -124,7 +124,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="ipAddr", - name="IP Address", + name="IP address", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 9a58cb385b6..25fdeb52114 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -14,15 +14,15 @@ from .entity import GoalZeroEntity SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="v12PortStatus", - name="12V Port Status", + name="12V port status", ), SwitchEntityDescription( key="usbPortStatus", - name="USB Port Status", + name="USB port status", ), SwitchEntityDescription( key="acPortStatus", - name="AC Port Status", + name="AC port status", ), ) diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py index ce67351c978..e61015a4925 100644 --- a/tests/components/goalzero/test_sensor.py +++ b/tests/components/goalzero/test_sensor.py @@ -85,7 +85,7 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_wifi_strength") + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_strength") assert state.state == "-62" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS From 11f80762eabf9e0a3c21395e7ae9cd8d939016ea Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Mon, 18 Jul 2022 11:17:25 +0100 Subject: [PATCH 526/820] Upgrade ness_alarm dependencies (#75298) * Upgrade ness alarm dependencies to fix #74571 * Update requirements --- homeassistant/components/ness_alarm/__init__.py | 1 - homeassistant/components/ness_alarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index d082f77d837..c81c3e0c7f7 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -99,7 +99,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: client = Client( host=host, port=port, - loop=hass.loop, update_interval=scan_interval.total_seconds(), infer_arming_state=infer_arming_state, ) diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 4aa01428d27..0b20ad7e6a9 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ness_alarm", "name": "Ness Alarm", "documentation": "https://www.home-assistant.io/integrations/ness_alarm", - "requirements": ["nessclient==0.9.15"], + "requirements": ["nessclient==0.10.0"], "codeowners": ["@nickw444"], "iot_class": "local_push", "loggers": ["nessclient"] diff --git a/requirements_all.txt b/requirements_all.txt index ad2c15cf77b..359b1541de8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ nad_receiver==0.3.0 ndms2_client==0.1.1 # homeassistant.components.ness_alarm -nessclient==0.9.15 +nessclient==0.10.0 # homeassistant.components.netdata netdata==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 784b740d459..450b4be10e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,7 +745,7 @@ mutesync==0.0.1 ndms2_client==0.1.1 # homeassistant.components.ness_alarm -nessclient==0.9.15 +nessclient==0.10.0 # homeassistant.components.discovery netdisco==3.0.0 From bfe34adf6ef1d5376d1b32dae2bdd70d61fba993 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Jul 2022 15:09:43 +0200 Subject: [PATCH 527/820] Update Home Assistant base image to 2022.07.0 (#75396) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 7bd4abcfc20..9cf66e2621a 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.06.2 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.06.2 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.06.2 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.06.2 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.06.2 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.07.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.07.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.07.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.07.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.07.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 9d0c91d6484c73b5390a0b9549847d0c11024583 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Jul 2022 15:13:46 +0200 Subject: [PATCH 528/820] Migrate Sensor.Community to new entity naming style (#75014) --- homeassistant/components/luftdaten/sensor.py | 1 + tests/components/luftdaten/test_sensor.py | 38 ++++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 055820de3a0..5333d86a708 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -97,6 +97,7 @@ class SensorCommunitySensor(CoordinatorEntity, SensorEntity): """Implementation of a Sensor.Community sensor.""" _attr_attribution = "Data provided by Sensor.Community" + _attr_has_entity_name = True _attr_should_poll = False def __init__( diff --git a/tests/components/luftdaten/test_sensor.py b/tests/components/luftdaten/test_sensor.py index 3cf4426d500..f3d0f1c0b1f 100644 --- a/tests/components/luftdaten/test_sensor.py +++ b/tests/components/luftdaten/test_sensor.py @@ -29,71 +29,73 @@ async def test_luftdaten_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - entry = entity_registry.async_get("sensor.temperature") + entry = entity_registry.async_get("sensor.sensor_12345_temperature") assert entry assert entry.device_id assert entry.unique_id == "12345_temperature" - state = hass.states.get("sensor.temperature") + state = hass.states.get("sensor.sensor_12345_temperature") assert state assert state.state == "22.3" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Temperature" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Temperature" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.humidity") + entry = entity_registry.async_get("sensor.sensor_12345_humidity") assert entry assert entry.device_id assert entry.unique_id == "12345_humidity" - state = hass.states.get("sensor.humidity") + state = hass.states.get("sensor.sensor_12345_humidity") assert state assert state.state == "34.7" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Humidity" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Humidity" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pressure") + entry = entity_registry.async_get("sensor.sensor_12345_pressure") assert entry assert entry.device_id assert entry.unique_id == "12345_pressure" - state = hass.states.get("sensor.pressure") + state = hass.states.get("sensor.sensor_12345_pressure") assert state assert state.state == "98545.0" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pressure" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Pressure" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pressure_at_sealevel") + entry = entity_registry.async_get("sensor.sensor_12345_pressure_at_sealevel") assert entry assert entry.device_id assert entry.unique_id == "12345_pressure_at_sealevel" - state = hass.states.get("sensor.pressure_at_sealevel") + state = hass.states.get("sensor.sensor_12345_pressure_at_sealevel") assert state assert state.state == "103102.13" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pressure at sealevel" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Pressure at sealevel" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pm10") + entry = entity_registry.async_get("sensor.sensor_12345_pm10") assert entry assert entry.device_id assert entry.unique_id == "12345_P1" - state = hass.states.get("sensor.pm10") + state = hass.states.get("sensor.sensor_12345_pm10") assert state assert state.state == "8.5" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "PM10" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 PM10" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( @@ -102,15 +104,15 @@ async def test_luftdaten_sensors( ) assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pm2_5") + entry = entity_registry.async_get("sensor.sensor_12345_pm2_5") assert entry assert entry.device_id assert entry.unique_id == "12345_P2" - state = hass.states.get("sensor.pm2_5") + state = hass.states.get("sensor.sensor_12345_pm2_5") assert state assert state.state == "4.07" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "PM2.5" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 PM2.5" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( From 6f5e4ca50354b4e9f1a02fbca70217ad14c3546d Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 18 Jul 2022 10:20:49 -0400 Subject: [PATCH 529/820] Fix ZHA light turn on issues (#75220) * rename variable * default transition is for color commands not level * no extra command for groups * don't transition color change when light off -> on * clean up * update condition * fix condition again... * simplify * simplify * missed one * rename * simplify * rename * tests * color_provided_while_off with no changes * fix missing flag clear * more tests for transition scenarios * add to comment * fix comment * don't transition when force on is set * stale comment * dont transition when colors don't change * remove extra line * remove debug print :) * fix colors * restore color to 65535 until investigated --- homeassistant/components/zha/light.py | 98 ++-- tests/components/zha/test_light.py | 811 +++++++++++++++++++++++++- 2 files changed, 868 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index e1c85b39d8e..cc8dc475c43 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -73,7 +73,6 @@ CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 -DEFAULT_TRANSITION = 1 DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 @@ -119,7 +118,7 @@ class BaseLight(LogMixin, light.LightEntity): """Operations common to all light entities.""" _FORCE_ON = False - _DEFAULT_COLOR_FROM_OFF_TRANSITION = 0 + _DEFAULT_MIN_TRANSITION_TIME = 0 def __init__(self, *args, **kwargs): """Initialize the light.""" @@ -140,7 +139,7 @@ class BaseLight(LogMixin, light.LightEntity): self._level_channel = None self._color_channel = None self._identify_channel = None - self._default_transition = None + self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes @property @@ -216,33 +215,49 @@ class BaseLight(LogMixin, light.LightEntity): transition = kwargs.get(light.ATTR_TRANSITION) duration = ( transition * 10 - if transition - else self._default_transition * 10 - if self._default_transition - else DEFAULT_TRANSITION - ) + if transition is not None + else self._zha_config_transition * 10 + ) or self._DEFAULT_MIN_TRANSITION_TIME # if 0 is passed in some devices still need the minimum default brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) + temperature = kwargs.get(light.ATTR_COLOR_TEMP) + hs_color = kwargs.get(light.ATTR_HS_COLOR) # If the light is currently off but a turn_on call with a color/temperature is sent, # the light needs to be turned on first at a low brightness level where the light is immediately transitioned # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness. # Otherwise, the transition is from the color the light had before being turned on to the new color. - # This can look especially bad with transitions longer than a second. - color_provided_from_off = ( - not self._state + # This can look especially bad with transitions longer than a second. We do not want to do this for + # devices that need to be forced to use the on command because we would end up with 4 commands sent: + # move to level, on, color, move to level... We also will not set this if the bulb is already in the + # desired color mode with the desired color or color temperature. + new_color_provided_while_off = ( + not isinstance(self, LightGroup) + and not self._FORCE_ON + and not self._state + and ( + ( + temperature is not None + and ( + self._color_temp != temperature + or self._attr_color_mode != ColorMode.COLOR_TEMP + ) + ) + or ( + hs_color is not None + and ( + self.hs_color != hs_color + or self._attr_color_mode != ColorMode.HS + ) + ) + ) and brightness_supported(self._attr_supported_color_modes) - and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs) ) - final_duration = duration - if color_provided_from_off: - # Set the duration for the color changing commands to 0. - duration = 0 if ( brightness is None - and (self._off_with_transition or color_provided_from_off) + and (self._off_with_transition or new_color_provided_while_off) and self._off_brightness is not None ): brightness = self._off_brightness @@ -254,11 +269,11 @@ class BaseLight(LogMixin, light.LightEntity): t_log = {} - if color_provided_from_off: + if new_color_provided_while_off: # If the light is currently off, we first need to turn it on at a low brightness level with no transition. # After that, we set it to the desired color/temperature with no transition. result = await self._level_channel.move_to_level_with_on_off( - DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_COLOR_FROM_OFF_TRANSITION + DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_MIN_TRANSITION_TIME ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -269,7 +284,7 @@ class BaseLight(LogMixin, light.LightEntity): if ( (brightness is not None or transition) - and not color_provided_from_off + and not new_color_provided_while_off and brightness_supported(self._attr_supported_color_modes) ): result = await self._level_channel.move_to_level_with_on_off( @@ -285,7 +300,7 @@ class BaseLight(LogMixin, light.LightEntity): if ( brightness is None - and not color_provided_from_off + and not new_color_provided_while_off or (self._FORCE_ON and brightness) ): # since some lights don't always turn on with move_to_level_with_on_off, @@ -297,9 +312,13 @@ class BaseLight(LogMixin, light.LightEntity): return self._state = True - if light.ATTR_COLOR_TEMP in kwargs: - temperature = kwargs[light.ATTR_COLOR_TEMP] - result = await self._color_channel.move_to_color_temp(temperature, duration) + if temperature is not None: + result = await self._color_channel.move_to_color_temp( + temperature, + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) t_log["move_to_color_temp"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) @@ -308,11 +327,14 @@ class BaseLight(LogMixin, light.LightEntity): self._color_temp = temperature self._hs_color = None - if light.ATTR_HS_COLOR in kwargs: - hs_color = kwargs[light.ATTR_HS_COLOR] + if hs_color is not None: xy_color = color_util.color_hs_to_xy(*hs_color) result = await self._color_channel.move_to_color( - int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, ) t_log["move_to_color"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -322,9 +344,9 @@ class BaseLight(LogMixin, light.LightEntity): self._hs_color = hs_color self._color_temp = None - if color_provided_from_off: + if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. - result = await self._level_channel.move_to_level(level, final_duration) + result = await self._level_channel.move_to_level(level, duration) t_log["move_to_level_if_color"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) @@ -371,12 +393,13 @@ class BaseLight(LogMixin, light.LightEntity): async def async_turn_off(self, **kwargs): """Turn the entity off.""" - duration = kwargs.get(light.ATTR_TRANSITION) + transition = kwargs.get(light.ATTR_TRANSITION) supports_level = brightness_supported(self._attr_supported_color_modes) - if duration and supports_level: + # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here + if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( - 0, duration * 10 + 0, transition * 10 ) else: result = await self._on_off_channel.off() @@ -387,7 +410,7 @@ class BaseLight(LogMixin, light.LightEntity): if supports_level: # store current brightness so that the next turn_on uses it. - self._off_with_transition = bool(duration) + self._off_with_transition = transition is not None self._off_brightness = self._brightness self.async_write_ha_state() @@ -460,7 +483,7 @@ class Light(BaseLight, ZhaEntity): if effect_list: self._effect_list = effect_list - self._default_transition = async_get_zha_config_value( + self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, CONF_DEFAULT_LIGHT_TRANSITION, @@ -472,6 +495,7 @@ class Light(BaseLight, ZhaEntity): """Set the state.""" self._state = bool(value) if value: + self._off_with_transition = False self._off_brightness = None self.async_write_ha_state() @@ -605,7 +629,7 @@ class HueLight(Light): @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, - manufacturers={"Jasco", "Quotra-Vision"}, + manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"}, ) class ForceOnLight(Light): """Representation of a light which does not respect move_to_level_with_on_off.""" @@ -621,7 +645,7 @@ class ForceOnLight(Light): class SengledLight(Light): """Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition.""" - _DEFAULT_COLOR_FROM_OFF_TRANSITION = 1 + _DEFAULT_MIN_TRANSITION_TIME = 1 @GROUP_MATCH() @@ -639,7 +663,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): self._color_channel = group.endpoint[Color.cluster_id] self._identify_channel = group.endpoint[Identify.cluster_id] self._debounced_member_refresh = None - self._default_transition = async_get_zha_config_value( + self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, CONF_DEFAULT_LIGHT_TRANSITION, diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index dd6df0dff19..982ff622341 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -15,7 +15,11 @@ from homeassistant.components.light import ( ColorMode, ) from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.light import FLASH_EFFECTS +from homeassistant.components.zha.light import ( + CAPABILITIES_COLOR_TEMP, + CAPABILITIES_COLOR_XY, + FLASH_EFFECTS, +) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform import homeassistant.util.dt as dt_util @@ -142,6 +146,10 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): ieee=IEEE_GROUPABLE_DEVICE, nwk=0xB79D, ) + color_cluster = zigpy_device.endpoints[1].light_color + color_cluster.PLUGGED_ATTR_READS = { + "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device @@ -167,8 +175,13 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE2, + manufacturer="Sengled", nwk=0xC79E, ) + color_cluster = zigpy_device.endpoints[1].light_color + color_cluster.PLUGGED_ATTR_READS = { + "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device @@ -201,6 +214,38 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined): return zha_device +@pytest.fixture +async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): + """Mock eWeLink light.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + general.Identify.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ieee="03:2d:6f:00:0a:90:69:e3", + manufacturer="eWeLink", + nwk=0xB79D, + ) + color_cluster = zigpy_device.endpoints[1].light_color + color_cluster.PLUGGED_ATTR_READS = { + "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + } + zha_device = await zha_device_joined(zigpy_device) + zha_device.available = True + return zha_device + + async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored): """Test zha light platform refresh.""" @@ -323,6 +368,758 @@ async def test_light( await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) +@patch( + "zigpy.zcl.clusters.lighting.Color.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.Identify.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.LevelControl.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.OnOff.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +async def test_transitions( + hass, device_light_1, device_light_2, eWeLink_light, coordinator +): + """Test ZHA light transition code.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + device_light_1._zha_gateway = zha_gateway + device_light_2._zha_gateway = zha_gateway + member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] + members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] + + assert coordinator.is_coordinator + + # test creating a group with 2 members + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) + await hass.async_block_till_done() + + assert zha_group is not None + assert len(zha_group.members) == 2 + for member in zha_group.members: + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None + + device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass) + device_2_entity_id = await find_entity_id(Platform.LIGHT, device_light_2, hass) + eWeLink_light_entity_id = await find_entity_id(Platform.LIGHT, eWeLink_light, hass) + assert device_1_entity_id != device_2_entity_id + + group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) + assert hass.states.get(group_entity_id) is not None + + assert device_1_entity_id in zha_group.member_entity_ids + assert device_2_entity_id in zha_group.member_entity_ids + + dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off + dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off + eWeLink_cluster_on_off = eWeLink_light.device.endpoints[1].on_off + + dev1_cluster_level = device_light_1.device.endpoints[1].level + dev2_cluster_level = device_light_2.device.endpoints[1].level + eWeLink_cluster_level = eWeLink_light.device.endpoints[1].level + + dev1_cluster_color = device_light_1.device.endpoints[1].light_color + dev2_cluster_color = device_light_2.device.endpoints[1].light_color + eWeLink_cluster_color = eWeLink_light.device.endpoints[1].light_color + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [device_light_1, device_light_2]) + await async_wait_for_updates(hass) + + # test that the lights were created and are off + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_OFF + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + # first test 0 length transition with no color provided + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_level.request.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_1_entity_id, "transition": 0, "brightness": 50}, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 50, # brightness (level in ZCL) + 0, # transition time + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 50 + + dev1_cluster_level.request.reset_mock() + + # test non 0 length transition with color provided while light is on + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "transition": 3, + "brightness": 18, + "color_temp": 432, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 18, # brightness (level in ZCL) + 30, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 432, # color temp mireds + 30.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 18 + assert light1_state.attributes["color_temp"] == 432 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # test 0 length transition to turn light off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + "transition": 0, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 0, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_OFF + + dev1_cluster_level.request.reset_mock() + + # test non 0 length transition and color temp while turning light on (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "transition": 1, + "brightness": 25, + "color_temp": 235, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 2 + assert dev1_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev1_cluster_level.request.call_args_list[0] == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 2, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_level.request.call_args_list[1] == call( + False, + 0, + dev1_cluster_level.commands_by_name["move_to_level"].schema, + 25, # brightness (level in ZCL) + 10.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 25 + assert light1_state.attributes["color_temp"] == 235 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # turn light 1 back off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + dev1_cluster_level.request.reset_mock() + + # test no transition provided and color temp while turning light on (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "brightness": 25, + "color_temp": 236, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 2 + assert dev1_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev1_cluster_level.request.call_args_list[0] == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 2, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 236, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_level.request.call_args_list[1] == call( + False, + 0, + dev1_cluster_level.commands_by_name["move_to_level"].schema, + 25, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 25 + assert light1_state.attributes["color_temp"] == 236 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # turn light 1 back off to setup group test + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + dev1_cluster_level.request.reset_mock() + + # test no transition when the same color temp is provided from off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "color_temp": 236, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + + assert dev1_cluster_on_off.request.call_args == call( + False, + 1, + dev1_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 236, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 25 + assert light1_state.attributes["color_temp"] == 236 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # turn light 1 back off to setup group test + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + dev1_cluster_level.request.reset_mock() + + # test sengled light uses default minimum transition time + dev2_cluster_on_off.request.reset_mock() + dev2_cluster_color.request.reset_mock() + dev2_cluster_level.request.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_2_entity_id, "transition": 0, "brightness": 100}, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 1 + assert dev2_cluster_level.request.await_count == 1 + assert dev2_cluster_level.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 100, # brightness (level in ZCL) + 1, # transition time - sengled light uses default minimum + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + assert light2_state.attributes["brightness"] == 100 + + dev2_cluster_level.request.reset_mock() + + # turn the sengled light back off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_2_entity_id, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 1 + assert dev2_cluster_on_off.request.await_count == 1 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 0 + assert dev2_cluster_level.request.await_count == 0 + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + dev2_cluster_on_off.request.reset_mock() + + # test non 0 length transition and color temp while turning light on and sengled (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_2_entity_id, + "transition": 1, + "brightness": 25, + "color_temp": 235, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 1 + assert dev2_cluster_color.request.await_count == 1 + assert dev2_cluster_level.request.call_count == 2 + assert dev2_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev2_cluster_level.request.call_args_list[0] == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 2, # brightness (level in ZCL) + 1, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev2_cluster_color.request.call_args == call( + False, + 10, + dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev2_cluster_level.request.call_args_list[1] == call( + False, + 0, + dev2_cluster_level.commands_by_name["move_to_level"].schema, + 25, # brightness (level in ZCL) + 10.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + assert light2_state.attributes["brightness"] == 25 + assert light2_state.attributes["color_temp"] == 235 + assert light2_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev2_cluster_level.request.reset_mock() + dev2_cluster_color.request.reset_mock() + + # turn the sengled light back off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_2_entity_id, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 1 + assert dev2_cluster_on_off.request.await_count == 1 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 0 + assert dev2_cluster_level.request.await_count == 0 + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + dev2_cluster_on_off.request.reset_mock() + + # test non 0 length transition and color temp while turning group light on (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": group_entity_id, + "transition": 1, + "brightness": 25, + "color_temp": 235, + }, + blocking=True, + ) + + group_on_off_channel = zha_group.endpoint[general.OnOff.cluster_id] + group_level_channel = zha_group.endpoint[general.LevelControl.cluster_id] + group_color_channel = zha_group.endpoint[lighting.Color.cluster_id] + assert group_on_off_channel.request.call_count == 0 + assert group_on_off_channel.request.await_count == 0 + assert group_color_channel.request.call_count == 1 + assert group_color_channel.request.await_count == 1 + assert group_level_channel.request.call_count == 1 + assert group_level_channel.request.await_count == 1 + + # groups are omitted from the 3 call dance for color_provided_while_off + assert group_color_channel.request.call_args == call( + False, + 10, + dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert group_level_channel.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 25, # brightness (level in ZCL) + 10.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_ON + assert group_state.attributes["brightness"] == 25 + assert group_state.attributes["color_temp"] == 235 + assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + group_on_off_channel.request.reset_mock() + group_color_channel.request.reset_mock() + group_level_channel.request.reset_mock() + + # turn the sengled light back on + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_2_entity_id, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 1 + assert dev2_cluster_on_off.request.await_count == 1 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 0 + assert dev2_cluster_level.request.await_count == 0 + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + + dev2_cluster_on_off.request.reset_mock() + + # turn the light off with a transition + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + {"entity_id": device_2_entity_id, "transition": 2}, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 1 + assert dev2_cluster_level.request.await_count == 1 + assert dev2_cluster_level.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 0, # brightness (level in ZCL) + 20, # transition time + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + dev2_cluster_level.request.reset_mock() + + # turn the light back on with no args should use a transition and last known brightness + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_2_entity_id}, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 1 + assert dev2_cluster_level.request.await_count == 1 + assert dev2_cluster_level.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 25, # brightness (level in ZCL) - this is the last brightness we set a few tests above + 1, # transition time - sengled light uses default minimum + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + + dev2_cluster_level.request.reset_mock() + + # test eWeLink color temp while turning light on from off (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": eWeLink_light_entity_id, + "color_temp": 235, + }, + blocking=True, + ) + assert eWeLink_cluster_on_off.request.call_count == 1 + assert eWeLink_cluster_on_off.request.await_count == 1 + assert eWeLink_cluster_color.request.call_count == 1 + assert eWeLink_cluster_color.request.await_count == 1 + assert eWeLink_cluster_level.request.call_count == 0 + assert eWeLink_cluster_level.request.await_count == 0 + + # first it comes on + assert eWeLink_cluster_on_off.request.call_args_list[0] == call( + False, + 1, + eWeLink_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + eWeLink_state = hass.states.get(eWeLink_light_entity_id) + assert eWeLink_state.state == STATE_ON + assert eWeLink_state.attributes["color_temp"] == 235 + assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light @@ -463,7 +1260,7 @@ async def async_test_level_on_off_from_hass( 4, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 10, - 1, + 0, expect_reply=True, manufacturer=None, tries=1, @@ -601,7 +1398,10 @@ async def test_zha_group_light_entity( # test that the lights were created and are off group_state = hass.states.get(group_entity_id) assert group_state.state == STATE_OFF - assert group_state.attributes["supported_color_modes"] == [ColorMode.HS] + assert group_state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] # Light which is off has no color mode assert "color_mode" not in group_state.attributes @@ -629,7 +1429,10 @@ async def test_zha_group_light_entity( # Check state group_state = hass.states.get(group_entity_id) assert group_state.state == STATE_ON - assert group_state.attributes["supported_color_modes"] == [ColorMode.HS] + assert group_state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] assert group_state.attributes["color_mode"] == ColorMode.HS # test long flashing the lights from the HA From b3ef6f4d047358048ecf12a4b4e07379cc90bcba Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 18 Jul 2022 09:18:07 -0600 Subject: [PATCH 530/820] Simplify Guardian entity inheritance hierarchy (#75274) --- homeassistant/components/guardian/__init__.py | 107 ++++++--------- .../components/guardian/binary_sensor.py | 127 +++++++++--------- homeassistant/components/guardian/button.py | 49 ++++--- homeassistant/components/guardian/sensor.py | 112 +++++++-------- homeassistant/components/guardian/switch.py | 78 ++++++----- 5 files changed, 225 insertions(+), 248 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index f13ca1a7ff5..d15b7d57aea 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable +from dataclasses import dataclass from typing import cast from aioguardian import Client @@ -24,10 +25,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( API_SENSOR_PAIR_DUMP, @@ -132,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # so we use a lock to ensure that only one API request is reaching it at a time: api_lock = asyncio.Lock() - # Set up DataUpdateCoordinators for the valve controller: + # Set up GuardianDataUpdateCoordinators for the valve controller: coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] for api, api_coro in ( @@ -418,17 +416,18 @@ class PairedSensorManager: dev_reg.async_remove_device(device.id) -class GuardianEntity(CoordinatorEntity): +class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]): """Define a base Guardian entity.""" _attr_has_entity_name = True - def __init__( # pylint: disable=super-init-not-called - self, entry: ConfigEntry, description: EntityDescription + def __init__( + self, coordinator: GuardianDataUpdateCoordinator, description: EntityDescription ) -> None: """Initialize.""" + super().__init__(coordinator) + self._attr_extra_state_attributes = {} - self._entry = entry self.entity_description = description @callback @@ -438,6 +437,17 @@ class GuardianEntity(CoordinatorEntity): This should be extended by Guardian platforms. """ + @callback + def _handle_coordinator_update(self) -> None: + """Respond to a DataUpdateCoordinator update.""" + self._async_update_from_latest_data() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._async_update_from_latest_data() + class PairedSensorEntity(GuardianEntity): """Define a Guardian paired sensor entity.""" @@ -445,11 +455,11 @@ class PairedSensorEntity(GuardianEntity): def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, + coordinator: GuardianDataUpdateCoordinator, description: EntityDescription, ) -> None: """Initialize.""" - super().__init__(entry, description) + super().__init__(coordinator, description) paired_sensor_uid = coordinator.data["uid"] self._attr_device_info = DeviceInfo( @@ -460,11 +470,20 @@ class PairedSensorEntity(GuardianEntity): via_device=(DOMAIN, entry.data[CONF_UID]), ) self._attr_unique_id = f"{paired_sensor_uid}_{description.key}" - self.coordinator = coordinator - async def async_added_to_hass(self) -> None: - """Perform tasks when the entity is added.""" - self._async_update_from_latest_data() + +@dataclass +class ValveControllerEntityDescriptionMixin: + """Define an entity description mixin for valve controller entities.""" + + api_category: str + + +@dataclass +class ValveControllerEntityDescription( + EntityDescription, ValveControllerEntityDescriptionMixin +): + """Describe a Guardian valve controller entity.""" class ValveControllerEntity(GuardianEntity): @@ -473,64 +492,18 @@ class ValveControllerEntity(GuardianEntity): def __init__( self, entry: ConfigEntry, - coordinators: dict[str, DataUpdateCoordinator], - description: EntityDescription, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerEntityDescription, ) -> None: """Initialize.""" - super().__init__(entry, description) + super().__init__(coordinators[description.api_category], description) + + self._diagnostics_coordinator = coordinators[API_SYSTEM_DIAGNOSTICS] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.data[CONF_UID])}, manufacturer="Elexa", - model=coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], + model=self._diagnostics_coordinator.data["firmware"], name=f"Guardian valve controller {entry.data[CONF_UID]}", ) self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" - self.coordinators = coordinators - - @property - def available(self) -> bool: - """Return if entity is available.""" - return any( - coordinator.last_update_success - for coordinator in self.coordinators.values() - ) - - async def _async_continue_entity_setup(self) -> None: - """Perform additional, internal tasks when the entity is about to be added. - - This should be extended by Guardian platforms. - """ - - @callback - def async_add_coordinator_update_listener(self, api: str) -> None: - """Add a listener to a DataUpdateCoordinator based on the API referenced.""" - - @callback - def update() -> None: - """Update the entity's state.""" - self._async_update_from_latest_data() - self.async_write_ha_state() - - self.async_on_remove(self.coordinators[api].async_add_listener(update)) - - async def async_added_to_hass(self) -> None: - """Perform tasks when the entity is added.""" - await self._async_continue_entity_setup() - self.async_add_coordinator_update_listener(API_SYSTEM_DIAGNOSTICS) - self._async_update_from_latest_data() - - async def async_update(self) -> None: - """Update the entity. - - Only used by the generic entity update service. - """ - # Ignore manual update requests if the entity is disabled - if not self.enabled: - return - - refresh_tasks = [ - coordinator.async_request_refresh() - for coordinator in self.coordinators.values() - ] - await asyncio.gather(*refresh_tasks) diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index dc9febcfa91..9b824ab589f 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -1,6 +1,8 @@ """Binary sensors for the Elexa Guardian integration.""" from __future__ import annotations +from dataclasses import dataclass + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -11,9 +13,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import PairedSensorEntity, ValveControllerEntity +from . import ( + PairedSensorEntity, + ValveControllerEntity, + ValveControllerEntityDescription, +) from .const import ( API_SYSTEM_ONBOARD_SENSOR_STATUS, API_WIFI_STATUS, @@ -23,6 +28,7 @@ from .const import ( DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) +from .util import GuardianDataUpdateCoordinator ATTR_CONNECTED_CLIENTS = "connected_clients" @@ -30,31 +36,42 @@ SENSOR_KIND_AP_INFO = "ap_enabled" SENSOR_KIND_LEAK_DETECTED = "leak_detected" SENSOR_KIND_MOVED = "moved" -SENSOR_DESCRIPTION_AP_ENABLED = BinarySensorEntityDescription( - key=SENSOR_KIND_AP_INFO, - name="Onboard AP enabled", - device_class=BinarySensorDeviceClass.CONNECTIVITY, - entity_category=EntityCategory.DIAGNOSTIC, -) -SENSOR_DESCRIPTION_LEAK_DETECTED = BinarySensorEntityDescription( - key=SENSOR_KIND_LEAK_DETECTED, - name="Leak detected", - device_class=BinarySensorDeviceClass.MOISTURE, -) -SENSOR_DESCRIPTION_MOVED = BinarySensorEntityDescription( - key=SENSOR_KIND_MOVED, - name="Recently moved", - device_class=BinarySensorDeviceClass.MOVING, - entity_category=EntityCategory.DIAGNOSTIC, -) + +@dataclass +class ValveControllerBinarySensorDescription( + BinarySensorEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller binary sensor.""" + PAIRED_SENSOR_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_LEAK_DETECTED, - SENSOR_DESCRIPTION_MOVED, + BinarySensorEntityDescription( + key=SENSOR_KIND_LEAK_DETECTED, + name="Leak detected", + device_class=BinarySensorDeviceClass.MOISTURE, + ), + BinarySensorEntityDescription( + key=SENSOR_KIND_MOVED, + name="Recently moved", + device_class=BinarySensorDeviceClass.MOVING, + entity_category=EntityCategory.DIAGNOSTIC, + ), ) + VALVE_CONTROLLER_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_AP_ENABLED, - SENSOR_DESCRIPTION_LEAK_DETECTED, + ValveControllerBinarySensorDescription( + key=SENSOR_KIND_LEAK_DETECTED, + name="Leak detected", + device_class=BinarySensorDeviceClass.MOISTURE, + api_category=API_SYSTEM_ONBOARD_SENSOR_STATUS, + ), + ValveControllerBinarySensorDescription( + key=SENSOR_KIND_AP_INFO, + name="Onboard AP enabled", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + api_category=API_WIFI_STATUS, + ), ) @@ -62,19 +79,18 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR] + valve_controller_coordinators = entry_data[DATA_COORDINATOR] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ - uid - ] - async_add_entities( - [ - PairedSensorBinarySensor(entry, coordinator, description) - for description in PAIRED_SENSOR_DESCRIPTIONS - ] + PairedSensorBinarySensor( + entry, paired_sensor_coordinators[uid], description + ) + for description in PAIRED_SENSOR_DESCRIPTIONS ) # Handle adding paired sensors after HASS startup: @@ -88,9 +104,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [ - ValveControllerBinarySensor( - entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description - ) + ValveControllerBinarySensor(entry, valve_controller_coordinators, description) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -98,9 +112,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorBinarySensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ].values() + for coordinator in paired_sensor_coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) @@ -111,10 +123,12 @@ async def async_setup_entry( class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): """Define a binary sensor related to a Guardian valve controller.""" + entity_description: BinarySensorEntityDescription + def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, + coordinator: GuardianDataUpdateCoordinator, description: BinarySensorEntityDescription, ) -> None: """Initialize.""" @@ -134,45 +148,26 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): """Define a binary sensor related to a Guardian valve controller.""" + entity_description: ValveControllerBinarySensorDescription + def __init__( self, entry: ConfigEntry, - coordinators: dict[str, DataUpdateCoordinator], - description: BinarySensorEntityDescription, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerBinarySensorDescription, ) -> None: """Initialize.""" super().__init__(entry, coordinators, description) self._attr_is_on = True - async def _async_continue_entity_setup(self) -> None: - """Add an API listener.""" - if self.entity_description.key == SENSOR_KIND_AP_INFO: - self.async_add_coordinator_update_listener(API_WIFI_STATUS) - elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: - self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS) - @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self.entity_description.key == SENSOR_KIND_AP_INFO: - self._attr_available = self.coordinators[ - API_WIFI_STATUS - ].last_update_success - self._attr_is_on = self.coordinators[API_WIFI_STATUS].data[ - "station_connected" - ] - self._attr_extra_state_attributes.update( - { - ATTR_CONNECTED_CLIENTS: self.coordinators[API_WIFI_STATUS].data.get( - "ap_clients" - ) - } - ) + self._attr_is_on = self.coordinator.data["station_connected"] + self._attr_extra_state_attributes[ + ATTR_CONNECTED_CLIENTS + ] = self.coordinator.data.get("ap_clients") elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: - self._attr_available = self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].last_update_success - self._attr_is_on = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ - "wet" - ] + self._attr_is_on = self.coordinator.data["wet"] diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index e7cc757d367..e013cde85d6 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -17,24 +17,26 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import ValveControllerEntity -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN +from . import ValveControllerEntity, ValveControllerEntityDescription +from .const import API_SYSTEM_DIAGNOSTICS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN +from .util import GuardianDataUpdateCoordinator @dataclass -class GuardianButtonDescriptionMixin: - """Define an entity description mixin for Guardian buttons.""" +class GuardianButtonEntityDescriptionMixin: + """Define an mixin for button entities.""" push_action: Callable[[Client], Awaitable] @dataclass -class GuardianButtonDescription( - ButtonEntityDescription, GuardianButtonDescriptionMixin +class ValveControllerButtonDescription( + ButtonEntityDescription, + ValveControllerEntityDescription, + GuardianButtonEntityDescriptionMixin, ): - """Describe a Guardian button description.""" + """Describe a Guardian valve controller button.""" BUTTON_KIND_REBOOT = "reboot" @@ -52,15 +54,21 @@ async def _async_valve_reset(client: Client) -> None: BUTTON_DESCRIPTIONS = ( - GuardianButtonDescription( + ValveControllerButtonDescription( key=BUTTON_KIND_REBOOT, name="Reboot", push_action=_async_reboot, + # Buttons don't actually need a coordinator; we give them one so they can + # properly inherit from GuardianEntity: + api_category=API_SYSTEM_DIAGNOSTICS, ), - GuardianButtonDescription( + ValveControllerButtonDescription( key=BUTTON_KIND_RESET_VALVE_DIAGNOSTICS, name="Reset valve diagnostics", push_action=_async_valve_reset, + # Buttons don't actually need a coordinator; we give them one so they can + # properly inherit from GuardianEntity: + api_category=API_SYSTEM_DIAGNOSTICS, ), ) @@ -69,16 +77,13 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian buttons based on a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + client = entry_data[DATA_CLIENT] + valve_controller_coordinators = entry_data[DATA_COORDINATOR] + async_add_entities( - [ - GuardianButton( - entry, - hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], - description, - ) - for description in BUTTON_DESCRIPTIONS - ] + GuardianButton(entry, valve_controller_coordinators, description, client) + for description in BUTTON_DESCRIPTIONS ) @@ -88,14 +93,14 @@ class GuardianButton(ValveControllerEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = EntityCategory.CONFIG - entity_description: GuardianButtonDescription + entity_description: ValveControllerButtonDescription def __init__( self, entry: ConfigEntry, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerButtonDescription, client: Client, - coordinators: dict[str, DataUpdateCoordinator], - description: GuardianButtonDescription, ) -> None: """Initialize.""" super().__init__(entry, coordinators, description) diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 895452e0fda..c4fd1e110fa 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -1,6 +1,8 @@ """Sensors for the Elexa Guardian integration.""" from __future__ import annotations +from dataclasses import dataclass + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -14,7 +16,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import PairedSensorEntity, ValveControllerEntity +from . import ( + PairedSensorEntity, + ValveControllerEntity, + ValveControllerEntityDescription, +) from .const import ( API_SYSTEM_DIAGNOSTICS, API_SYSTEM_ONBOARD_SENSOR_STATUS, @@ -29,35 +35,47 @@ SENSOR_KIND_BATTERY = "battery" SENSOR_KIND_TEMPERATURE = "temperature" SENSOR_KIND_UPTIME = "uptime" -SENSOR_DESCRIPTION_BATTERY = SensorEntityDescription( - key=SENSOR_KIND_BATTERY, - name="Battery", - device_class=SensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, -) -SENSOR_DESCRIPTION_TEMPERATURE = SensorEntityDescription( - key=SENSOR_KIND_TEMPERATURE, - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_FAHRENHEIT, - state_class=SensorStateClass.MEASUREMENT, -) -SENSOR_DESCRIPTION_UPTIME = SensorEntityDescription( - key=SENSOR_KIND_UPTIME, - name="Uptime", - icon="mdi:timer", - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=TIME_MINUTES, -) + +@dataclass +class ValveControllerSensorDescription( + SensorEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller sensor.""" + PAIRED_SENSOR_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_BATTERY, - SENSOR_DESCRIPTION_TEMPERATURE, + SensorEntityDescription( + key=SENSOR_KIND_BATTERY, + name="Battery", + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key=SENSOR_KIND_TEMPERATURE, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + ), ) VALVE_CONTROLLER_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_TEMPERATURE, - SENSOR_DESCRIPTION_UPTIME, + ValveControllerSensorDescription( + key=SENSOR_KIND_TEMPERATURE, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + api_category=API_SYSTEM_ONBOARD_SENSOR_STATUS, + ), + ValveControllerSensorDescription( + key=SENSOR_KIND_UPTIME, + name="Uptime", + icon="mdi:timer", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=TIME_MINUTES, + api_category=API_SYSTEM_DIAGNOSTICS, + ), ) @@ -65,19 +83,16 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR] + valve_controller_coordinators = entry_data[DATA_COORDINATOR] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ - uid - ] - async_add_entities( - [ - PairedSensorSensor(entry, coordinator, description) - for description in PAIRED_SENSOR_DESCRIPTIONS - ] + PairedSensorSensor(entry, paired_sensor_coordinators[uid], description) + for description in PAIRED_SENSOR_DESCRIPTIONS ) # Handle adding paired sensors after HASS startup: @@ -91,9 +106,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorSensor | ValveControllerSensor] = [ - ValveControllerSensor( - entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description - ) + ValveControllerSensor(entry, valve_controller_coordinators, description) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -101,9 +114,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorSensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ].values() + for coordinator in paired_sensor_coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) @@ -114,6 +125,8 @@ async def async_setup_entry( class PairedSensorSensor(PairedSensorEntity, SensorEntity): """Define a binary sensor related to a Guardian valve controller.""" + entity_description: SensorEntityDescription + @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" @@ -126,25 +139,12 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): class ValveControllerSensor(ValveControllerEntity, SensorEntity): """Define a generic Guardian sensor.""" - async def _async_continue_entity_setup(self) -> None: - """Register API interest (and related tasks) when the entity is added.""" - if self.entity_description.key == SENSOR_KIND_TEMPERATURE: - self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS) + entity_description: ValveControllerSensorDescription @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self.entity_description.key == SENSOR_KIND_TEMPERATURE: - self._attr_available = self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].last_update_success - self._attr_native_value = self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].data["temperature"] + self._attr_native_value = self.coordinator.data["temperature"] elif self.entity_description.key == SENSOR_KIND_UPTIME: - self._attr_available = self.coordinators[ - API_SYSTEM_DIAGNOSTICS - ].last_update_success - self._attr_native_value = self.coordinators[API_SYSTEM_DIAGNOSTICS].data[ - "uptime" - ] + self._attr_native_value = self.coordinator.data["uptime"] diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 485b0a3ffbc..c58f4548a87 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,6 +1,7 @@ """Switches for the Elexa Guardian integration.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any from aioguardian import Client @@ -11,10 +12,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import ValveControllerEntity +from . import ValveControllerEntity, ValveControllerEntityDescription from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN +from .util import GuardianDataUpdateCoordinator ATTR_AVG_CURRENT = "average_current" ATTR_INST_CURRENT = "instantaneous_current" @@ -23,10 +24,21 @@ ATTR_TRAVEL_COUNT = "travel_count" SWITCH_KIND_VALVE = "valve" -SWITCH_DESCRIPTION_VALVE = SwitchEntityDescription( - key=SWITCH_KIND_VALVE, - name="Valve controller", - icon="mdi:water", + +@dataclass +class ValveControllerSwitchDescription( + SwitchEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller switch.""" + + +VALVE_CONTROLLER_DESCRIPTIONS = ( + ValveControllerSwitchDescription( + key=SWITCH_KIND_VALVE, + name="Valve controller", + icon="mdi:water", + api_category=API_VALVE_STATUS, + ), ) @@ -34,61 +46,53 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + client = entry_data[DATA_CLIENT] + valve_controller_coordinators = entry_data[DATA_COORDINATOR] + async_add_entities( - [ - ValveControllerSwitch( - entry, - hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], - ) - ] + ValveControllerSwitch(entry, valve_controller_coordinators, description, client) + for description in VALVE_CONTROLLER_DESCRIPTIONS ) class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): """Define a switch to open/close the Guardian valve.""" + entity_description: ValveControllerSwitchDescription + + ON_STATES = { + "start_opening", + "opening", + "finish_opening", + "opened", + } + def __init__( self, entry: ConfigEntry, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerSwitchDescription, client: Client, - coordinators: dict[str, DataUpdateCoordinator], ) -> None: """Initialize.""" - super().__init__(entry, coordinators, SWITCH_DESCRIPTION_VALVE) + super().__init__(entry, coordinators, description) self._attr_is_on = True self._client = client - async def _async_continue_entity_setup(self) -> None: - """Register API interest (and related tasks) when the entity is added.""" - self.async_add_coordinator_update_listener(API_VALVE_STATUS) - @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" - self._attr_available = self.coordinators[API_VALVE_STATUS].last_update_success - self._attr_is_on = self.coordinators[API_VALVE_STATUS].data["state"] in ( - "start_opening", - "opening", - "finish_opening", - "opened", - ) - + self._attr_is_on = self.coordinator.data["state"] in self.ON_STATES self._attr_extra_state_attributes.update( { - ATTR_AVG_CURRENT: self.coordinators[API_VALVE_STATUS].data[ - "average_current" - ], - ATTR_INST_CURRENT: self.coordinators[API_VALVE_STATUS].data[ - "instantaneous_current" - ], - ATTR_INST_CURRENT_DDT: self.coordinators[API_VALVE_STATUS].data[ + ATTR_AVG_CURRENT: self.coordinator.data["average_current"], + ATTR_INST_CURRENT: self.coordinator.data["instantaneous_current"], + ATTR_INST_CURRENT_DDT: self.coordinator.data[ "instantaneous_current_ddt" ], - ATTR_TRAVEL_COUNT: self.coordinators[API_VALVE_STATUS].data[ - "travel_count" - ], + ATTR_TRAVEL_COUNT: self.coordinator.data["travel_count"], } ) From 3144d179e0073f780c8d6738457f9e455c641339 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 18 Jul 2022 17:39:38 +0200 Subject: [PATCH 531/820] Make UniFi utilise forward_entry_setups (#74835) --- homeassistant/components/unifi/__init__.py | 50 ++++------ homeassistant/components/unifi/config_flow.py | 14 +-- homeassistant/components/unifi/controller.py | 94 ++++++++++--------- tests/components/unifi/test_controller.py | 24 ++--- tests/components/unifi/test_init.py | 47 ++++------ 5 files changed, 103 insertions(+), 126 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 1369bb69e1b..086bae8d8cf 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -5,19 +5,13 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from .const import ( - ATTR_MANUFACTURER, - CONF_CONTROLLER, - DOMAIN as UNIFI_DOMAIN, - LOGGER, - UNIFI_WIRELESS_CLIENTS, -) -from .controller import UniFiController +from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN, UNIFI_WIRELESS_CLIENTS +from .controller import PLATFORMS, UniFiController, get_unifi_controller +from .errors import AuthenticationRequired, CannotConnect from .services import async_setup_services, async_unload_services SAVE_DELAY = 10 @@ -40,9 +34,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Flat configuration was introduced with 2021.3 await async_flatten_entry_data(hass, config_entry) - controller = UniFiController(hass, config_entry) - if not await controller.async_setup(): - return False + try: + api = await get_unifi_controller(hass, config_entry.data) + controller = UniFiController(hass, config_entry, api) + await controller.initialize() + + except CannotConnect as err: + raise ConfigEntryNotReady from err + + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err # Unique ID was introduced with 2021.3 if config_entry.unique_id is None: @@ -50,30 +51,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry, unique_id=controller.site_id ) - if not hass.data[UNIFI_DOMAIN]: + hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + await controller.async_update_device_registry() + + if len(hass.data[UNIFI_DOMAIN]) == 1: async_setup_services(hass) - hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller + api.start_websocket() config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) ) - LOGGER.debug("UniFi Network config options %s", config_entry.options) - - if controller.mac is None: - return True - - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - configuration_url=controller.api.url, - connections={(CONNECTION_NETWORK_MAC, controller.mac)}, - default_manufacturer=ATTR_MANUFACTURER, - default_model="UniFi Network", - default_name="UniFi Network", - ) - return True diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 2f49c15e4d8..4944dd91296 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -9,6 +9,7 @@ from __future__ import annotations from collections.abc import Mapping import socket +from types import MappingProxyType from typing import Any from urllib.parse import urlparse @@ -46,7 +47,7 @@ from .const import ( DEFAULT_POE_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) -from .controller import UniFiController, get_controller +from .controller import UniFiController, get_unifi_controller from .errors import AuthenticationRequired, CannotConnect DEFAULT_PORT = 443 @@ -99,16 +100,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): } try: - controller = await get_controller( - self.hass, - host=self.config[CONF_HOST], - username=self.config[CONF_USERNAME], - password=self.config[CONF_PASSWORD], - port=self.config[CONF_PORT], - site=self.config[CONF_SITE_ID], - verify_ssl=self.config[CONF_VERIFY_SSL], + controller = await get_unifi_controller( + self.hass, MappingProxyType(self.config) ) - sites = await controller.sites() except AuthenticationRequired: diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index fa92568b477..7446d6abbff 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -4,6 +4,8 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta import ssl +from types import MappingProxyType +from typing import Any from aiohttp import CookieJar import aiounifi @@ -36,14 +38,19 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, entity_registry as er +from homeassistant.helpers import ( + aiohttp_client, + device_registry as dr, + entity_registry as er, +) +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.dt as dt_util from .const import ( + ATTR_MANUFACTURER, CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, @@ -91,12 +98,15 @@ DEVICE_CONNECTED = ( class UniFiController: """Manages a single UniFi Network instance.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry, api): """Initialize the system.""" self.hass = hass self.config_entry = config_entry + self.api = api + + api.callback = self.async_unifi_signalling_callback + self.available = True - self.api = None self.progress = None self.wireless_clients = None @@ -295,36 +305,18 @@ class UniFiController: unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry) - async def async_setup(self): + async def initialize(self): """Set up a UniFi Network instance.""" - try: - self.api = await get_controller( - self.hass, - host=self.config_entry.data[CONF_HOST], - username=self.config_entry.data[CONF_USERNAME], - password=self.config_entry.data[CONF_PASSWORD], - port=self.config_entry.data[CONF_PORT], - site=self.config_entry.data[CONF_SITE_ID], - verify_ssl=self.config_entry.data[CONF_VERIFY_SSL], - async_callback=self.async_unifi_signalling_callback, - ) - await self.api.initialize() - - sites = await self.api.sites() - description = await self.api.site_description() - - except CannotConnect as err: - raise ConfigEntryNotReady from err - - except AuthenticationRequired as err: - raise ConfigEntryAuthFailed from err + await self.api.initialize() + sites = await self.api.sites() for site in sites.values(): if self.site == site["name"]: self.site_id = site["_id"] self._site_name = site["desc"] break + description = await self.api.site_description() self._site_role = description[0]["site_role"] # Restore clients that are not a part of active clients list. @@ -357,18 +349,12 @@ class UniFiController: self.wireless_clients = wireless_clients.get_data(self.config_entry) self.update_wireless_clients() - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) - - self.api.start_websocket() - self.config_entry.add_update_listener(self.async_config_entry_updated) self._cancel_heartbeat_check = async_track_time_interval( self.hass, self._async_check_for_stale, CHECK_HEARTBEAT_INTERVAL ) - return True - @callback def async_heartbeat( self, unique_id: str, heartbeat_expire_time: datetime | None = None @@ -397,6 +383,22 @@ class UniFiController: for unique_id in unique_ids_to_remove: del self._heartbeat_time[unique_id] + async def async_update_device_registry(self) -> None: + """Update device registry.""" + if self.mac is None: + return + + device_registry = dr.async_get(self.hass) + + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + configuration_url=self.api.url, + connections={(CONNECTION_NETWORK_MAC, self.mac)}, + default_manufacturer=ATTR_MANUFACTURER, + default_model="UniFi Network", + default_name="UniFi Network", + ) + @staticmethod async def async_config_entry_updated( hass: HomeAssistant, config_entry: ConfigEntry @@ -463,13 +465,14 @@ class UniFiController: return True -async def get_controller( - hass, host, username, password, port, site, verify_ssl, async_callback=None -): +async def get_unifi_controller( + hass: HomeAssistant, + config: MappingProxyType[str, Any], +) -> aiounifi.Controller: """Create a controller object and verify authentication.""" sslcontext = None - if verify_ssl: + if verify_ssl := bool(config.get(CONF_VERIFY_SSL)): session = aiohttp_client.async_get_clientsession(hass) if isinstance(verify_ssl, str): sslcontext = ssl.create_default_context(cafile=verify_ssl) @@ -479,14 +482,13 @@ async def get_controller( ) controller = aiounifi.Controller( - host, - username=username, - password=password, - port=port, - site=site, + host=config[CONF_HOST], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + port=config[CONF_PORT], + site=config[CONF_SITE_ID], websession=session, sslcontext=sslcontext, - callback=async_callback, ) try: @@ -498,7 +500,7 @@ async def get_controller( except aiounifi.Unauthorized as err: LOGGER.warning( "Connected to UniFi Network at %s but not registered: %s", - host, + config[CONF_HOST], err, ) raise AuthenticationRequired from err @@ -510,13 +512,15 @@ async def get_controller( aiounifi.RequestError, aiounifi.ResponseError, ) as err: - LOGGER.error("Error connecting to the UniFi Network at %s: %s", host, err) + LOGGER.error( + "Error connecting to the UniFi Network at %s: %s", config[CONF_HOST], err + ) raise CannotConnect from err except aiounifi.LoginRequired as err: LOGGER.warning( "Connected to UniFi Network at %s but login required: %s", - host, + config[CONF_HOST], err, ) raise AuthenticationRequired from err diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 625afbb4ec6..e420d031f46 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -30,7 +30,7 @@ from homeassistant.components.unifi.const import ( from homeassistant.components.unifi.controller import ( PLATFORMS, RETRY_TIMER, - get_controller, + get_unifi_controller, ) from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.const import ( @@ -271,7 +271,7 @@ async def test_controller_mac(hass, aioclient_mock): async def test_controller_not_accessible(hass): """Retry to login gets scheduled when connection fails.""" with patch( - "homeassistant.components.unifi.controller.get_controller", + "homeassistant.components.unifi.controller.get_unifi_controller", side_effect=CannotConnect, ): await setup_unifi_integration(hass) @@ -281,7 +281,7 @@ async def test_controller_not_accessible(hass): async def test_controller_trigger_reauth_flow(hass): """Failed authentication trigger a reauthentication flow.""" with patch( - "homeassistant.components.unifi.controller.get_controller", + "homeassistant.components.unifi.get_unifi_controller", side_effect=AuthenticationRequired, ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await setup_unifi_integration(hass) @@ -292,7 +292,7 @@ async def test_controller_trigger_reauth_flow(hass): async def test_controller_unknown_error(hass): """Unknown errors are handled.""" with patch( - "homeassistant.components.unifi.controller.get_controller", + "homeassistant.components.unifi.controller.get_unifi_controller", side_effect=Exception, ): await setup_unifi_integration(hass) @@ -470,22 +470,22 @@ async def test_reconnect_mechanism_exceptions( mock_reconnect.assert_called_once() -async def test_get_controller(hass): +async def test_get_unifi_controller(hass): """Successful call.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await get_controller(hass, **CONTROLLER_DATA) + assert await get_unifi_controller(hass, CONTROLLER_DATA) -async def test_get_controller_verify_ssl_false(hass): +async def test_get_unifi_controller_verify_ssl_false(hass): """Successful call with verify ssl set to false.""" controller_data = dict(CONTROLLER_DATA) controller_data[CONF_VERIFY_SSL] = False with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await get_controller(hass, **controller_data) + assert await get_unifi_controller(hass, controller_data) @pytest.mark.parametrize( @@ -501,9 +501,11 @@ async def test_get_controller_verify_ssl_false(hass): (aiounifi.AiounifiException, AuthenticationRequired), ], ) -async def test_get_controller_fails_to_connect(hass, side_effect, raised_exception): - """Check that get_controller can handle controller being unavailable.""" +async def test_get_unifi_controller_fails_to_connect( + hass, side_effect, raised_exception +): + """Check that get_unifi_controller can handle controller being unavailable.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", side_effect=side_effect ), pytest.raises(raised_exception): - await get_controller(hass, **CONTROLLER_DATA) + await get_unifi_controller(hass, CONTROLLER_DATA) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index f183e1c22ff..03ea89097c5 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,10 +1,10 @@ """Test UniFi Network integration setup process.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from homeassistant.components import unifi from homeassistant.components.unifi import async_flatten_entry_data from homeassistant.components.unifi.const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN -from homeassistant.helpers import device_registry as dr +from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.setup import async_setup_component from .test_controller import ( @@ -29,40 +29,27 @@ async def test_successful_config_entry(hass, aioclient_mock): assert hass.data[UNIFI_DOMAIN] -async def test_controller_fail_setup(hass): - """Test that a failed setup still stores controller.""" - with patch("homeassistant.components.unifi.UniFiController") as mock_controller: - mock_controller.return_value.async_setup = AsyncMock(return_value=False) +async def test_setup_entry_fails_config_entry_not_ready(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.unifi.get_unifi_controller", + side_effect=CannotConnect, + ): await setup_unifi_integration(hass) assert hass.data[UNIFI_DOMAIN] == {} -async def test_controller_mac(hass): - """Test that configured options for a host are loaded via config entry.""" - entry = MockConfigEntry( - domain=UNIFI_DOMAIN, data=ENTRY_CONFIG, unique_id="1", entry_id=1 - ) - entry.add_to_hass(hass) +async def test_setup_entry_fails_trigger_reauth_flow(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.unifi.get_unifi_controller", + side_effect=AuthenticationRequired, + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await setup_unifi_integration(hass) + mock_flow_init.assert_called_once() - with patch("homeassistant.components.unifi.UniFiController") as mock_controller: - mock_controller.return_value.async_setup = AsyncMock(return_value=True) - mock_controller.return_value.mac = "mac1" - mock_controller.return_value.api.url = "https://123:443" - assert await unifi.async_setup_entry(hass, entry) is True - - assert len(mock_controller.mock_calls) == 2 - - device_registry = dr.async_get(hass) - device = device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "mac1")}, - ) - assert device.configuration_url == "https://123:443" - assert device.manufacturer == "Ubiquiti Networks" - assert device.model == "UniFi Network" - assert device.name == "UniFi Network" - assert device.sw_version is None + assert hass.data[UNIFI_DOMAIN] == {} async def test_flatten_entry_data(hass): From 7adb0f0ef5432e79c445f714e6783841acd3b9a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Jul 2022 22:10:22 +0200 Subject: [PATCH 532/820] Custom component -> Custom integration (#75404) --- homeassistant/components/number/__init__.py | 4 ++-- homeassistant/components/sensor/recorder.py | 2 +- homeassistant/components/weather/__init__.py | 2 +- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/frame.py | 2 +- homeassistant/util/async_.py | 2 +- tests/components/sensor/test_init.py | 2 +- tests/components/sensor/test_recorder.py | 2 +- tests/helpers/test_aiohttp_client.py | 2 +- tests/helpers/test_entity.py | 2 +- tests/helpers/test_httpx_client.py | 2 +- tests/util/test_async.py | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 5ab1eaa7be4..4990fbfa7f8 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -152,7 +152,7 @@ class NumberEntityDescription(EntityDescription): else: module = inspect.getmodule(self) if module and module.__file__ and "custom_components" in module.__file__: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " @@ -222,7 +222,7 @@ class NumberEntity(Entity): ): module = inspect.getmodule(cls) if module and module.__file__ and "custom_components" in module.__file__: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 3fc5cbec7ee..ea7d129a9c3 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -275,7 +275,7 @@ def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: custom_component = entity_sources(hass).get(entity_id, {}).get("custom_component") report_issue = "" if custom_component: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 3e0233917d7..c55ae043622 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -299,7 +299,7 @@ class WeatherEntity(Entity): and module.__file__ and "custom_components" in module.__file__ ): - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f00f7d85e76..cb71cfd9edf 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -936,7 +936,7 @@ class Entity(ABC): """Suggest to report an issue.""" report_issue = "" if "custom_components" in type(self).__module__: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index b81f5f29432..ca5cf759d8e 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -96,7 +96,7 @@ def report_integration( index = found_frame.filename.index(path) if path == "custom_components/": - extra = " to the custom component author" + extra = " to the custom integration author" else: extra = "" diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 8b96e85664d..e9c5a41062e 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -150,7 +150,7 @@ def check_loop( integration = found_frame.filename[start:end] if path == "custom_components/": - extra = " to the custom component author" + extra = " to the custom integration author" else: extra = "" diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 0bae8235ff9..3a593b0e6cc 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -107,7 +107,7 @@ async def test_deprecated_last_reset( f"with state_class {state_class} has set last_reset. Setting last_reset for " "entities with state_class other than 'total' is not supported. Please update " "your configuration if state_class is manually configured, otherwise report it " - "to the custom component author." + "to the custom integration author." ) in caplog.text state = hass.states.get("sensor.test") diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index c62d1309c7a..4be59e4c82c 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -775,7 +775,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( ( "sensor.custom_sensor", "from integration test ", - "report it to the custom component author", + "report it to the custom integration author", ), ], ) diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 1ffb4267167..7bac31e8e19 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -183,7 +183,7 @@ async def test_warning_close_session_custom(hass, caplog): await session.close() assert ( "Detected integration that closes the Home Assistant aiohttp session. " - "Please report issue to the custom component author for hue using this method at " + "Please report issue to the custom integration author for hue using this method at " "custom_components/hue/light.py, line 23: await session.close()" in caplog.text ) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index b9067a3db1c..698d3cfe98a 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -715,7 +715,7 @@ async def test_warn_slow_write_state_custom_component(hass, caplog): assert ( "Updating state for comp_test.test_entity " "(.CustomComponentEntity'>) " - "took 10.000 seconds. Please report it to the custom component author." + "took 10.000 seconds. Please report it to the custom integration author." ) in caplog.text diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index cdb650f7686..068c0fe1470 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -153,6 +153,6 @@ async def test_warning_close_session_custom(hass, caplog): await httpx_session.aclose() assert ( "Detected integration that closes the Home Assistant httpx client. " - "Please report issue to the custom component author for hue using this method at " + "Please report issue to the custom integration author for hue using this method at " "custom_components/hue/light.py, line 23: await session.aclose()" in caplog.text ) diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 9bae6f5ebea..10861767de1 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -168,7 +168,7 @@ async def test_check_loop_async_custom(caplog): hasync.check_loop(banned_function) assert ( "Detected blocking call to banned_function inside the event loop. This is " - "causing stability issues. Please report issue to the custom component author " + "causing stability issues. Please report issue to the custom integration author " "for hue doing blocking calls at custom_components/hue/light.py, line 23: " "self.light.is_on" in caplog.text ) From 8b912d1d91c6376f79f213769e5c0ab72570d1b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 15:28:14 -0500 Subject: [PATCH 533/820] Significantly improve BLE reliablity with linux/dbus for HKC (#75410) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 192a5cb701a..10ca50db820 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.7"], + "requirements": ["aiohomekit==1.1.8"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 359b1541de8..30af50ccc15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.7 +aiohomekit==1.1.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 450b4be10e1..e679fb6827a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.7 +aiohomekit==1.1.8 # homeassistant.components.emulated_hue # homeassistant.components.http From 5928a7d494cd3ba91c92c4ffc598bb0f5d07077d Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 19 Jul 2022 07:02:58 +1000 Subject: [PATCH 534/820] Correct devices in Advantage Air (#75395) --- .../components/advantage_air/binary_sensor.py | 2 +- homeassistant/components/advantage_air/climate.py | 1 - homeassistant/components/advantage_air/entity.py | 8 +++++--- homeassistant/components/advantage_air/select.py | 2 +- homeassistant/components/advantage_air/sensor.py | 2 +- homeassistant/components/advantage_air/switch.py | 2 +- .../components/advantage_air/test_binary_sensor.py | 12 ++++++------ tests/components/advantage_air/test_climate.py | 6 +++--- tests/components/advantage_air/test_cover.py | 10 +++++----- tests/components/advantage_air/test_select.py | 2 +- tests/components/advantage_air/test_sensor.py | 14 +++++++------- tests/components/advantage_air/test_switch.py | 2 +- 12 files changed, 32 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index c87bf37ca92..9fc53d7e1dc 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -43,11 +43,11 @@ class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.PROBLEM _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_name = "Filter" def __init__(self, instance, ac_key): """Initialize an Advantage Air Filter sensor.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} filter' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-filter' ) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 192e1987902..1d89c313579 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -108,7 +108,6 @@ class AdvantageAirAC(AdvantageAirClimateEntity): def __init__(self, instance, ac_key): """Initialize an AdvantageAir AC unit.""" super().__init__(instance, ac_key) - self._attr_name = self._ac["name"] self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{ac_key}' if self._ac.get("myAutoModeEnabled"): self._attr_hvac_modes = AC_HVAC_MODES + [HVACMode.AUTO] diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index b0ff1bfb8c8..6c434518656 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -18,11 +18,13 @@ class AdvantageAirEntity(CoordinatorEntity): self.ac_key = ac_key self.zone_key = zone_key self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])}, + via_device=(DOMAIN, self.coordinator.data["system"]["rid"]), + identifiers={ + (DOMAIN, f"{self.coordinator.data['system']['rid']}_{ac_key}") + }, manufacturer="Advantage Air", model=self.coordinator.data["system"]["sysType"], - name=self.coordinator.data["system"]["name"], - sw_version=self.coordinator.data["system"]["myAppRev"], + name=self._ac["name"], ) @property diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index 0edae258279..5dfa92c10ad 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -32,11 +32,11 @@ class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): _attr_options = [ADVANTAGE_AIR_INACTIVE] _number_to_name = {0: ADVANTAGE_AIR_INACTIVE} _name_to_number = {ADVANTAGE_AIR_INACTIVE: 0} + _attr_name = "MyZone" def __init__(self, instance, ac_key): """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} myZone' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-myzone' ) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 855a0e6d15f..370aab7b292 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -67,7 +67,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): super().__init__(instance, ac_key) self.action = action self._time_key = f"countDownTo{action}" - self._attr_name = f'{self._ac["name"]} time to {action}' + self._attr_name = f"Time to {action}" self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}' ) diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 3c0060c65b3..504578c72e2 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -32,11 +32,11 @@ class AdvantageAirFreshAir(AdvantageAirEntity, SwitchEntity): """Representation of Advantage Air fresh air control.""" _attr_icon = "mdi:air-filter" + _attr_name = "Fresh air" def __init__(self, instance, ac_key): """Initialize an Advantage Air fresh air control.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} Fresh Air' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-freshair' ) diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 9aa092a8679..4bd792808fb 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -34,7 +34,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test First Air Filter - entity_id = "binary_sensor.testname_ac_one_filter" + entity_id = "binary_sensor.ac_one_filter" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -44,7 +44,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-filter" # Test Second Air Filter - entity_id = "binary_sensor.testname_ac_two_filter" + entity_id = "binary_sensor.ac_two_filter" state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -54,7 +54,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac2-filter" # Test First Motion Sensor - entity_id = "binary_sensor.testname_zone_open_with_sensor_motion" + entity_id = "binary_sensor.ac_one_zone_open_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -64,7 +64,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-motion" # Test Second Motion Sensor - entity_id = "binary_sensor.testname_zone_closed_with_sensor_motion" + entity_id = "binary_sensor.ac_one_zone_closed_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -74,7 +74,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-motion" # Test First MyZone Sensor (disabled by default) - entity_id = "binary_sensor.testname_zone_open_with_sensor_myzone" + entity_id = "binary_sensor.ac_one_zone_open_with_sensor_myzone" assert not hass.states.get(entity_id) @@ -96,7 +96,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-myzone" # Test Second Motion Sensor (disabled by default) - entity_id = "binary_sensor.testname_zone_closed_with_sensor_myzone" + entity_id = "binary_sensor.ac_one_zone_closed_with_sensor_myzone" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 47073f27fc1..6ee0a614b09 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -51,7 +51,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Main Climate Entity - entity_id = "climate.testname_ac_one" + entity_id = "climate.ac_one" state = hass.states.get(entity_id) assert state assert state.state == HVACMode.FAN_ONLY @@ -122,7 +122,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test Climate Zone Entity - entity_id = "climate.testname_zone_open_with_sensor" + entity_id = "climate.ac_one_zone_open_with_sensor" state = hass.states.get(entity_id) assert state assert state.attributes.get("min_temp") == 16 @@ -204,7 +204,7 @@ async def test_climate_async_failed_update(hass, aioclient_mock): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: ["climate.testname_ac_one"], ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: ["climate.ac_one"], ATTR_TEMPERATURE: 25}, blocking=True, ) assert len(aioclient_mock.mock_calls) == 2 diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index c638c6a3c87..90cdf9a2168 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -45,7 +45,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Cover Zone Entity - entity_id = "cover.testname_zone_open_without_sensor" + entity_id = "cover.ac_two_zone_open_without_sensor" state = hass.states.get(entity_id) assert state assert state.state == STATE_OPEN @@ -119,8 +119,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_CLOSE_COVER, { ATTR_ENTITY_ID: [ - "cover.testname_zone_open_without_sensor", - "cover.testname_zone_closed_without_sensor", + "cover.ac_two_zone_open_without_sensor", + "cover.ac_two_zone_closed_without_sensor", ] }, blocking=True, @@ -134,8 +134,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_OPEN_COVER, { ATTR_ENTITY_ID: [ - "cover.testname_zone_open_without_sensor", - "cover.testname_zone_closed_without_sensor", + "cover.ac_two_zone_open_without_sensor", + "cover.ac_two_zone_closed_without_sensor", ] }, blocking=True, diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index 2ba982fc384..20d42fdcffc 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -37,7 +37,7 @@ async def test_select_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test MyZone Select Entity - entity_id = "select.testname_ac_one_myzone" + entity_id = "select.ac_one_myzone" state = hass.states.get(entity_id) assert state assert state.state == "Zone open with Sensor" diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 70322f4c9df..4dc2f1baaff 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test First TimeToOn Sensor - entity_id = "sensor.testname_ac_one_time_to_on" + entity_id = "sensor.ac_one_time_to_on" state = hass.states.get(entity_id) assert state assert int(state.state) == 0 @@ -66,7 +66,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test First TimeToOff Sensor - entity_id = "sensor.testname_ac_one_time_to_off" + entity_id = "sensor.ac_one_time_to_off" state = hass.states.get(entity_id) assert state assert int(state.state) == 10 @@ -91,7 +91,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test First Zone Vent Sensor - entity_id = "sensor.testname_zone_open_with_sensor_vent" + entity_id = "sensor.ac_one_zone_open_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 100 @@ -101,7 +101,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-vent" # Test Second Zone Vent Sensor - entity_id = "sensor.testname_zone_closed_with_sensor_vent" + entity_id = "sensor.ac_one_zone_closed_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 0 @@ -111,7 +111,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-vent" # Test First Zone Signal Sensor - entity_id = "sensor.testname_zone_open_with_sensor_signal" + entity_id = "sensor.ac_one_zone_open_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 40 @@ -121,7 +121,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-signal" # Test Second Zone Signal Sensor - entity_id = "sensor.testname_zone_closed_with_sensor_signal" + entity_id = "sensor.ac_one_zone_closed_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 10 @@ -131,7 +131,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-signal" # Test First Zone Temp Sensor (disabled by default) - entity_id = "sensor.testname_zone_open_with_sensor_temperature" + entity_id = "sensor.ac_one_zone_open_with_sensor_temperature" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py index 41af9e8ff80..be78edf8ffe 100644 --- a/tests/components/advantage_air/test_switch.py +++ b/tests/components/advantage_air/test_switch.py @@ -41,7 +41,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Switch Entity - entity_id = "switch.testname_ac_one_fresh_air" + entity_id = "switch.ac_one_fresh_air" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF From 45d1f8bc55e72c7bbe5f8e011b05e8a1d13f8718 Mon Sep 17 00:00:00 2001 From: stegm Date: Mon, 18 Jul 2022 23:08:18 +0200 Subject: [PATCH 535/820] Address late review of kostal plenticore (#75297) * Changtes from review #64927 * Fix unit tests for number. * Changes from review. --- .../components/kostal_plenticore/__init__.py | 2 +- .../components/kostal_plenticore/const.py | 54 ---- .../components/kostal_plenticore/number.py | 72 ++++- .../kostal_plenticore/test_number.py | 265 +++++++++--------- 4 files changed, 201 insertions(+), 192 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index 6c7a7cde523..24e8ab9f0d3 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -12,7 +12,7 @@ from .helper import Plenticore _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.NUMBER] +PLATFORMS = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 11bb794f799..7ae0b13f0e8 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,8 +1,6 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" -from dataclasses import dataclass from typing import NamedTuple -from homeassistant.components.number import NumberEntityDescription from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -18,7 +16,6 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) -from homeassistant.helpers.entity import EntityCategory DOMAIN = "kostal_plenticore" @@ -794,57 +791,6 @@ SENSOR_PROCESS_DATA = [ ] -@dataclass -class PlenticoreNumberEntityDescriptionMixin: - """Define an entity description mixin for number entities.""" - - module_id: str - data_id: str - fmt_from: str - fmt_to: str - - -@dataclass -class PlenticoreNumberEntityDescription( - NumberEntityDescription, PlenticoreNumberEntityDescriptionMixin -): - """Describes a Plenticore number entity.""" - - -NUMBER_SETTINGS_DATA = [ - PlenticoreNumberEntityDescription( - key="battery_min_soc", - entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=False, - icon="mdi:battery-negative", - name="Battery min SoC", - native_unit_of_measurement=PERCENTAGE, - native_max_value=100, - native_min_value=5, - native_step=5, - module_id="devices:local", - data_id="Battery:MinSoc", - fmt_from="format_round", - fmt_to="format_round_back", - ), - PlenticoreNumberEntityDescription( - key="battery_min_home_consumption", - device_class=SensorDeviceClass.POWER, - entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=False, - name="Battery min Home Consumption", - native_unit_of_measurement=POWER_WATT, - native_max_value=38000, - native_min_value=50, - native_step=1, - module_id="devices:local", - data_id="Battery:MinHomeComsumption", - fmt_from="format_round", - fmt_to="format_round_back", - ), -] - - class SwitchData(NamedTuple): """Representation of a SelectData tuple.""" diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py index 1ad911f6d15..69fba631b34 100644 --- a/homeassistant/components/kostal_plenticore/number.py +++ b/homeassistant/components/kostal_plenticore/number.py @@ -2,25 +2,82 @@ from __future__ import annotations from abc import ABC +from dataclasses import dataclass from datetime import timedelta -from functools import partial import logging from kostal.plenticore import SettingsData -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, POWER_WATT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, NUMBER_SETTINGS_DATA, PlenticoreNumberEntityDescription +from .const import DOMAIN from .helper import PlenticoreDataFormatter, SettingDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +@dataclass +class PlenticoreNumberEntityDescriptionMixin: + """Define an entity description mixin for number entities.""" + + module_id: str + data_id: str + fmt_from: str + fmt_to: str + + +@dataclass +class PlenticoreNumberEntityDescription( + NumberEntityDescription, PlenticoreNumberEntityDescriptionMixin +): + """Describes a Plenticore number entity.""" + + +NUMBER_SETTINGS_DATA = [ + PlenticoreNumberEntityDescription( + key="battery_min_soc", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:battery-negative", + name="Battery min SoC", + native_unit_of_measurement=PERCENTAGE, + native_max_value=100, + native_min_value=5, + native_step=5, + module_id="devices:local", + data_id="Battery:MinSoc", + fmt_from="format_round", + fmt_to="format_round_back", + ), + PlenticoreNumberEntityDescription( + key="battery_min_home_consumption", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + name="Battery min Home Consumption", + native_unit_of_measurement=POWER_WATT, + native_max_value=38000, + native_min_value=50, + native_step=1, + module_id="devices:local", + data_id="Battery:MinHomeComsumption", + fmt_from="format_round", + fmt_to="format_round_back", + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -54,10 +111,9 @@ async def async_setup_entry( continue setting_data = next( - filter( - partial(lambda id, sd: id == sd.id, description.data_id), - available_settings_data[description.module_id], - ) + sd + for sd in available_settings_data[description.module_id] + if description.data_id == sd.id ) entities.append( diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index f0fb42f6e78..f6978612cff 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -1,72 +1,98 @@ """Test Kostal Plenticore number.""" -from unittest.mock import AsyncMock, MagicMock +from collections.abc import Generator +from datetime import timedelta +from unittest.mock import patch -from kostal.plenticore import SettingsData +from kostal.plenticore import PlenticoreApiClient, SettingsData import pytest -from homeassistant.components.kostal_plenticore.const import ( - PlenticoreNumberEntityDescription, +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, ) -from homeassistant.components.kostal_plenticore.number import PlenticoreDataNumber +from homeassistant.components.number.const import ATTR_MAX, ATTR_MIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture -def mock_coordinator() -> MagicMock: - """Return a mocked coordinator for tests.""" - coordinator = MagicMock() - coordinator.async_write_data = AsyncMock() - coordinator.async_refresh = AsyncMock() - return coordinator +def mock_plenticore_client() -> Generator[PlenticoreApiClient, None, None]: + """Return a patched PlenticoreApiClient.""" + with patch( + "homeassistant.components.kostal_plenticore.helper.PlenticoreApiClient", + autospec=True, + ) as plenticore_client_class: + yield plenticore_client_class.return_value @pytest.fixture -def mock_number_description() -> PlenticoreNumberEntityDescription: - """Return a PlenticoreNumberEntityDescription for tests.""" - return PlenticoreNumberEntityDescription( - key="mock key", - module_id="moduleid", - data_id="dataid", - native_min_value=0, - native_max_value=1000, - fmt_from="format_round", - fmt_to="format_round_back", - ) +def mock_get_setting_values(mock_plenticore_client: PlenticoreApiClient) -> list: + """Add a setting value to the given Plenticore client. + Returns a list with setting values which can be extended by test cases. + """ -@pytest.fixture -def mock_setting_data() -> SettingsData: - """Return a default SettingsData for tests.""" - return SettingsData( - { - "default": None, - "min": None, - "access": None, - "max": None, - "unit": None, - "type": None, - "id": "data_id", - } - ) - - -async def test_setup_all_entries( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore: MagicMock -): - """Test if all available entries are setup up.""" - mock_plenticore.client.get_settings.return_value = { + mock_plenticore_client.get_settings.return_value = { "devices:local": [ - SettingsData({"id": "Battery:MinSoc", "min": None, "max": None}), SettingsData( - {"id": "Battery:MinHomeComsumption", "min": None, "max": None} + { + "default": None, + "min": 5, + "max": 100, + "access": "readwrite", + "unit": "%", + "type": "byte", + "id": "Battery:MinSoc", + } + ), + SettingsData( + { + "default": None, + "min": 50, + "max": 38000, + "access": "readwrite", + "unit": "W", + "type": "byte", + "id": "Battery:MinHomeComsumption", + } ), ] } + # this values are always retrieved by the integration on startup + setting_values = [ + { + "devices:local": { + "Properties:SerialNo": "42", + "Branding:ProductName1": "PLENTICORE", + "Branding:ProductName2": "plus 10", + "Properties:VersionIOC": "01.45", + "Properties:VersionMC": " 01.46", + }, + "scb:network": {"Hostname": "scb"}, + } + ] + + mock_plenticore_client.get_setting_values.side_effect = setting_values + + return setting_values + + +async def test_setup_all_entries( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, +): + """Test if all available entries are setup.""" + mock_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -78,10 +104,16 @@ async def test_setup_all_entries( async def test_setup_no_entries( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore: MagicMock + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test that no entries are setup up.""" - mock_plenticore.client.get_settings.return_value = [] + """Test that no entries are setup if Plenticore does not provide data.""" + + mock_plenticore_client.get_settings.return_value = [] + mock_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -92,106 +124,81 @@ async def test_setup_no_entries( assert ent_reg.async_get("number.scb_battery_min_home_consumption") is None -def test_number_returns_value_if_available( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, +async def test_number_has_value( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test if value property on PlenticoreDataNumber returns an int if available.""" + """Test if number has a value if data is provided on update.""" - mock_coordinator.data = {"moduleid": {"dataid": "42"}} + mock_get_setting_values.append({"devices:local": {"Battery:MinSoc": "42"}}) - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) + mock_config_entry.add_to_hass(hass) - assert entity.value == 42 - assert type(entity.value) == int + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + + state = hass.states.get("number.scb_battery_min_soc") + assert state.state == "42" + assert state.attributes[ATTR_MIN] == 5 + assert state.attributes[ATTR_MAX] == 100 -def test_number_returns_none_if_unavailable( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, +async def test_number_is_unavailable( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test if value property on PlenticoreDataNumber returns none if unavailable.""" + """Test if number is unavailable if no data is provided on update.""" - mock_coordinator.data = {} # makes entity not available + mock_config_entry.add_to_hass(hass) - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() - assert entity.value is None + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + + state = hass.states.get("number.scb_battery_min_soc") + assert state.state == STATE_UNAVAILABLE async def test_set_value( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test if set value calls coordinator with new value.""" + """Test if a new value could be set.""" - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) + mock_get_setting_values.append({"devices:local": {"Battery:MinSoc": "42"}}) - await entity.async_set_native_value(42) + mock_config_entry.add_to_hass(hass) - mock_coordinator.async_write_data.assert_called_once_with( - "moduleid", {"dataid": "42"} - ) - mock_coordinator.async_refresh.assert_called_once() + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() -async def test_minmax_overwrite( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, -): - """Test if min/max value is overwritten from retrieved settings data.""" - - setting_data = SettingsData( + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, { - "min": "5", - "max": "100", - } + ATTR_ENTITY_ID: "number.scb_battery_min_soc", + ATTR_VALUE: 80, + }, + blocking=True, ) - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, setting_data + mock_plenticore_client.set_setting_values.assert_called_once_with( + "devices:local", {"Battery:MinSoc": "80"} ) - - assert entity.min_value == 5 - assert entity.max_value == 100 - - -async def test_added_to_hass( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, -): - """Test if coordinator starts fetching after added to hass.""" - - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) - - await entity.async_added_to_hass() - - mock_coordinator.start_fetch_data.assert_called_once_with("moduleid", "dataid") - - -async def test_remove_from_hass( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, -): - """Test if coordinator stops fetching after remove from hass.""" - - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) - - await entity.async_will_remove_from_hass() - - mock_coordinator.stop_fetch_data.assert_called_once_with("moduleid", "dataid") From 9f33a0d6dd3c645dbfb3e6cb292d3c6669c55823 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 18 Jul 2022 17:22:38 -0400 Subject: [PATCH 536/820] Migrate Tautulli to new entity naming style (#75382) --- homeassistant/components/tautulli/__init__.py | 2 ++ homeassistant/components/tautulli/sensor.py | 29 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index af7939a45f5..bd5032014bc 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -46,6 +46,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class TautulliEntity(CoordinatorEntity[TautulliDataUpdateCoordinator]): """Defines a base Tautulli entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: TautulliDataUpdateCoordinator, diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 1d5efde7cc7..5653aa5dc57 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -60,14 +60,14 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( icon="mdi:plex", key="watching_count", - name="Tautulli", + name="Watching", native_unit_of_measurement="Watching", value_fn=lambda home_stats, activity, _: cast(int, activity.stream_count), ), TautulliSensorEntityDescription( icon="mdi:plex", key="stream_count_direct_play", - name="Direct Plays", + name="Direct plays", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="Streams", entity_registry_enabled_default=False, @@ -78,7 +78,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( icon="mdi:plex", key="stream_count_direct_stream", - name="Direct Streams", + name="Direct streams", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="Streams", entity_registry_enabled_default=False, @@ -99,7 +99,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), TautulliSensorEntityDescription( key="total_bandwidth", - name="Total Bandwidth", + name="Total bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DATA_KILOBITS, state_class=SensorStateClass.MEASUREMENT, @@ -107,7 +107,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), TautulliSensorEntityDescription( key="lan_bandwidth", - name="LAN Bandwidth", + name="LAN bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DATA_KILOBITS, entity_registry_enabled_default=False, @@ -116,7 +116,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), TautulliSensorEntityDescription( key="wan_bandwidth", - name="WAN Bandwidth", + name="WAN bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DATA_KILOBITS, entity_registry_enabled_default=False, @@ -126,21 +126,21 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( icon="mdi:movie-open", key="top_movies", - name="Top Movie", + name="Top movie", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( icon="mdi:television", key="top_tv", - name="Top TV Show", + name="Top TV show", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( icon="mdi:walk", key=ATTR_TOP_USER, - name="Top User", + name="Top user", entity_registry_enabled_default=False, value_fn=get_top_stats, ), @@ -170,7 +170,7 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( ), TautulliSessionSensorEntityDescription( key="full_title", - name="Full Title", + name="Full title", entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.full_title), ), @@ -184,7 +184,7 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( ), TautulliSessionSensorEntityDescription( key="stream_resolution", - name="Stream Resolution", + name="Stream resolution", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.stream_video_resolution), @@ -192,21 +192,21 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( TautulliSessionSensorEntityDescription( icon="mdi:plex", key="transcode_decision", - name="Transcode Decision", + name="Transcode decision", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.transcode_decision), ), TautulliSessionSensorEntityDescription( key="session_thumb", - name="session Thumbnail", + name="session thumbnail", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.user_thumb), ), TautulliSessionSensorEntityDescription( key="video_resolution", - name="Video Resolution", + name="Video resolution", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.video_resolution), @@ -284,7 +284,6 @@ class TautulliSessionSensor(TautulliEntity, SensorEntity): super().__init__(coordinator, description, user) entry_id = coordinator.config_entry.entry_id self._attr_unique_id = f"{entry_id}_{user.user_id}_{description.key}" - self._attr_name = f"{user.username} {description.name}" @property def native_value(self) -> StateType: From e75d7dfb7515d3d17c4df0aa08336f50d97d6d2b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Jul 2022 23:34:53 +0200 Subject: [PATCH 537/820] Update google-cloud-texttospeech to 2.12.0 (#75401) --- homeassistant/components/google_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 633c5edc453..d48571c55bd 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==2.11.1"], + "requirements": ["google-cloud-texttospeech==2.12.0"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 30af50ccc15..4b7743aa53e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -743,7 +743,7 @@ goodwe==0.2.15 google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==2.11.1 +google-cloud-texttospeech==2.12.0 # homeassistant.components.nest google-nest-sdm==2.0.0 From 983bcfa9355fb86462bdfde7611dfbf7642aaf82 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 18 Jul 2022 17:41:06 -0400 Subject: [PATCH 538/820] Bump AIOAladdinConnect to 0.1.27 (#75400) --- homeassistant/components/aladdin_connect/cover.py | 7 ++++--- homeassistant/components/aladdin_connect/manifest.json | 2 +- homeassistant/components/aladdin_connect/model.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/aladdin_connect/conftest.py | 1 + tests/components/aladdin_connect/test_cover.py | 5 +++++ 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index d06ebebd076..f032fcecbe0 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -89,6 +89,7 @@ class AladdinDevice(CoverEntity): self._device_id = device["device_id"] self._number = device["door_number"] self._name = device["name"] + self._serial = device["serial"] self._attr_unique_id = f"{self._device_id}-{self._number}" self._attr_has_entity_name = True @@ -108,8 +109,8 @@ class AladdinDevice(CoverEntity): """Schedule a state update.""" self.async_write_ha_state() - self._acc.register_callback(update_callback, self._number) - await self._acc.get_doors(self._number) + self._acc.register_callback(update_callback, self._serial) + await self._acc.get_doors(self._serial) async def async_will_remove_from_hass(self) -> None: """Close Aladdin Connect before removing.""" @@ -125,7 +126,7 @@ class AladdinDevice(CoverEntity): async def async_update(self) -> None: """Update status of cover.""" - await self._acc.get_doors(self._number) + await self._acc.get_doors(self._serial) @property def is_closed(self) -> bool | None: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 5baeba33971..4a04bf69aed 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.25"], + "requirements": ["AIOAladdinConnect==0.1.27"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py index 4248f3504fe..63624b223a9 100644 --- a/homeassistant/components/aladdin_connect/model.py +++ b/homeassistant/components/aladdin_connect/model.py @@ -11,3 +11,4 @@ class DoorDevice(TypedDict): door_number: int name: str status: str + serial: str diff --git a/requirements_all.txt b/requirements_all.txt index 4b7743aa53e..92e8fa4f9e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.25 +AIOAladdinConnect==0.1.27 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e679fb6827a..5153abbf447 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.25 +AIOAladdinConnect==0.1.27 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index ee68d207361..c8f7d240ba5 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -10,6 +10,7 @@ DEVICE_CONFIG_OPEN = { "name": "home", "status": "open", "link_status": "Connected", + "serial": "12345", } diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index 54ec4ee5de1..4e65607fa9d 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -33,6 +33,7 @@ DEVICE_CONFIG_OPEN = { "name": "home", "status": "open", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_OPENING = { @@ -41,6 +42,7 @@ DEVICE_CONFIG_OPENING = { "name": "home", "status": "opening", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_CLOSED = { @@ -49,6 +51,7 @@ DEVICE_CONFIG_CLOSED = { "name": "home", "status": "closed", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_CLOSING = { @@ -57,6 +60,7 @@ DEVICE_CONFIG_CLOSING = { "name": "home", "status": "closing", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_DISCONNECTED = { @@ -65,6 +69,7 @@ DEVICE_CONFIG_DISCONNECTED = { "name": "home", "status": "open", "link_status": "Disconnected", + "serial": "12345", } DEVICE_CONFIG_BAD = { From 135495297774dbe114ede3989b612b949f160dd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 17:56:34 -0500 Subject: [PATCH 539/820] Migrate LIFX to config entry per device (#74316) --- .coveragerc | 3 - .strict-typing | 1 + CODEOWNERS | 3 +- homeassistant/components/lifx/__init__.py | 195 +++- homeassistant/components/lifx/config_flow.py | 242 ++++- homeassistant/components/lifx/const.py | 16 + homeassistant/components/lifx/coordinator.py | 158 +++ homeassistant/components/lifx/discovery.py | 58 + homeassistant/components/lifx/light.py | 946 ++++------------- homeassistant/components/lifx/manager.py | 216 ++++ homeassistant/components/lifx/manifest.json | 10 +- homeassistant/components/lifx/migration.py | 74 ++ homeassistant/components/lifx/strings.json | 22 +- .../components/lifx/translations/en.json | 24 +- homeassistant/components/lifx/util.py | 161 +++ homeassistant/generated/dhcp.py | 2 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 9 + tests/components/lifx/__init__.py | 217 ++++ tests/components/lifx/conftest.py | 57 + tests/components/lifx/test_config_flow.py | 508 +++++++++ tests/components/lifx/test_init.py | 150 +++ tests/components/lifx/test_light.py | 993 ++++++++++++++++++ tests/components/lifx/test_migration.py | 281 +++++ 25 files changed, 3601 insertions(+), 759 deletions(-) create mode 100644 homeassistant/components/lifx/coordinator.py create mode 100644 homeassistant/components/lifx/discovery.py create mode 100644 homeassistant/components/lifx/manager.py create mode 100644 homeassistant/components/lifx/migration.py create mode 100644 homeassistant/components/lifx/util.py create mode 100644 tests/components/lifx/__init__.py create mode 100644 tests/components/lifx/conftest.py create mode 100644 tests/components/lifx/test_config_flow.py create mode 100644 tests/components/lifx/test_init.py create mode 100644 tests/components/lifx/test_light.py create mode 100644 tests/components/lifx/test_migration.py diff --git a/.coveragerc b/.coveragerc index 0fb2210b3cd..3645286980b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -647,9 +647,6 @@ omit = homeassistant/components/life360/const.py homeassistant/components/life360/coordinator.py homeassistant/components/life360/device_tracker.py - homeassistant/components/lifx/__init__.py - homeassistant/components/lifx/const.py - homeassistant/components/lifx/light.py homeassistant/components/lifx_cloud/scene.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py diff --git a/.strict-typing b/.strict-typing index 9792f401ac4..aa911dd81d9 100644 --- a/.strict-typing +++ b/.strict-typing @@ -146,6 +146,7 @@ homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* homeassistant.components.light.* +homeassistant.components.lifx.* homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.logbook.* diff --git a/CODEOWNERS b/CODEOWNERS index d2ad6ef2307..fd271e6adc6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -577,7 +577,8 @@ build.json @home-assistant/supervisor /homeassistant/components/lg_netcast/ @Drafteed /homeassistant/components/life360/ @pnbruckner /tests/components/life360/ @pnbruckner -/homeassistant/components/lifx/ @Djelibeybi +/homeassistant/components/lifx/ @bdraco @Djelibeybi +/tests/components/lifx/ @bdraco @Djelibeybi /homeassistant/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core /homeassistant/components/linux_battery/ @fabaff diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index b6710064a74..8816226ff84 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -1,19 +1,41 @@ """Support for LIFX.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +from datetime import datetime, timedelta +import socket +from typing import Any + +from aiolifx.aiolifx import Light +from aiolifx_connection import LIFXConnection import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STARTED, + Platform, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN, TARGET_ANY +from .coordinator import LIFXUpdateCoordinator +from .discovery import async_discover_devices, async_trigger_discovery +from .manager import LIFXManager +from .migration import async_migrate_entities_devices, async_migrate_legacy_entries +from .util import async_entry_is_legacy, async_get_legacy_entry CONF_SERVER = "server" CONF_BROADCAST = "broadcast" + INTERFACE_SCHEMA = vol.Schema( { vol.Optional(CONF_SERVER): cv.string, @@ -22,39 +44,176 @@ INTERFACE_SCHEMA = vol.Schema( } ) -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: {LIGHT_DOMAIN: vol.Schema(vol.All(cv.ensure_list, [INTERFACE_SCHEMA]))}}, - extra=vol.ALLOW_EXTRA, +CONFIG_SCHEMA = vol.All( + cv.deprecated(DOMAIN), + vol.Schema( + { + DOMAIN: { + LIGHT_DOMAIN: vol.Schema(vol.All(cv.ensure_list, [INTERFACE_SCHEMA])) + } + }, + extra=vol.ALLOW_EXTRA, + ), ) -DATA_LIFX_MANAGER = "lifx_manager" PLATFORMS = [Platform.LIGHT] +DISCOVERY_INTERVAL = timedelta(minutes=15) +MIGRATION_INTERVAL = timedelta(minutes=5) + +DISCOVERY_COOLDOWN = 5 + + +async def async_legacy_migration( + hass: HomeAssistant, + legacy_entry: ConfigEntry, + discovered_devices: Iterable[Light], +) -> bool: + """Migrate config entries.""" + existing_serials = { + entry.unique_id + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.unique_id and not async_entry_is_legacy(entry) + } + # device.mac_addr is not the mac_address, its the serial number + hosts_by_serial = {device.mac_addr: device.ip_addr for device in discovered_devices} + missing_discovery_count = await async_migrate_legacy_entries( + hass, hosts_by_serial, existing_serials, legacy_entry + ) + if missing_discovery_count: + _LOGGER.info( + "Migration in progress, waiting to discover %s device(s)", + missing_discovery_count, + ) + return False + + _LOGGER.debug( + "Migration successful, removing legacy entry %s", legacy_entry.entry_id + ) + await hass.config_entries.async_remove(legacy_entry.entry_id) + return True + + +class LIFXDiscoveryManager: + """Manage discovery and migration.""" + + def __init__(self, hass: HomeAssistant, migrating: bool) -> None: + """Init the manager.""" + self.hass = hass + self.lock = asyncio.Lock() + self.migrating = migrating + self._cancel_discovery: CALLBACK_TYPE | None = None + + @callback + def async_setup_discovery_interval(self) -> None: + """Set up discovery at an interval.""" + if self._cancel_discovery: + self._cancel_discovery() + self._cancel_discovery = None + discovery_interval = ( + MIGRATION_INTERVAL if self.migrating else DISCOVERY_INTERVAL + ) + _LOGGER.debug( + "LIFX starting discovery with interval: %s and migrating: %s", + discovery_interval, + self.migrating, + ) + self._cancel_discovery = async_track_time_interval( + self.hass, self.async_discovery, discovery_interval + ) + + async def async_discovery(self, *_: Any) -> None: + """Discovery and migrate LIFX devics.""" + migrating_was_in_progress = self.migrating + + async with self.lock: + discovered = await async_discover_devices(self.hass) + + if legacy_entry := async_get_legacy_entry(self.hass): + migration_complete = await async_legacy_migration( + self.hass, legacy_entry, discovered + ) + if migration_complete and migrating_was_in_progress: + self.migrating = False + _LOGGER.debug( + "LIFX migration complete, switching to normal discovery interval: %s", + DISCOVERY_INTERVAL, + ) + self.async_setup_discovery_interval() + + if discovered: + async_trigger_discovery(self.hass, discovered) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LIFX component.""" - conf = config.get(DOMAIN) + hass.data[DOMAIN] = {} + migrating = bool(async_get_legacy_entry(hass)) + discovery_manager = LIFXDiscoveryManager(hass, migrating) - hass.data[DOMAIN] = conf or {} + @callback + def _async_delayed_discovery(now: datetime) -> None: + """Start an untracked task to discover devices. - if conf is not None: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - ) + We do not want the discovery task to block startup. + """ + asyncio.create_task(discovery_manager.async_discovery()) + + # Let the system settle a bit before starting discovery + # to reduce the risk we miss devices because the event + # loop is blocked at startup. + discovery_manager.async_setup_discovery_interval() + async_call_later(hass, DISCOVERY_COOLDOWN, _async_delayed_discovery) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, discovery_manager.async_discovery + ) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LIFX from a config entry.""" + + if async_entry_is_legacy(entry): + return True + + if legacy_entry := async_get_legacy_entry(hass): + # If the legacy entry still exists, harvest the entities + # that are moving to this config entry. + await async_migrate_entities_devices(hass, legacy_entry.entry_id, entry) + + assert entry.unique_id is not None + domain_data = hass.data[DOMAIN] + if DATA_LIFX_MANAGER not in domain_data: + manager = LIFXManager(hass) + domain_data[DATA_LIFX_MANAGER] = manager + manager.async_setup() + + host = entry.data[CONF_HOST] + connection = LIFXConnection(host, TARGET_ANY) + try: + await connection.async_setup() + except socket.gaierror as ex: + raise ConfigEntryNotReady(f"Could not resolve {host}: {ex}") from ex + coordinator = LIFXUpdateCoordinator(hass, connection, entry.title) + coordinator.async_setup() + await coordinator.async_config_entry_first_refresh() + + domain_data[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - hass.data.pop(DATA_LIFX_MANAGER).cleanup() - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if async_entry_is_legacy(entry): + return True + domain_data = hass.data[DOMAIN] + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: LIFXUpdateCoordinator = domain_data.pop(entry.entry_id) + coordinator.connection.async_stop() + # Only the DATA_LIFX_MANAGER left, remove it. + if len(domain_data) == 1: + manager: LIFXManager = domain_data.pop(DATA_LIFX_MANAGER) + manager.async_unload() + return unload_ok diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index c48bee9e4e7..30b42e640f8 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -1,16 +1,240 @@ """Config flow flow LIFX.""" -import aiolifx +from __future__ import annotations -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_flow +import asyncio +import socket +from typing import Any -from .const import DOMAIN +from aiolifx.aiolifx import Light +from aiolifx_connection import LIFXConnection +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.components.dhcp import DhcpServiceInfo +from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import DiscoveryInfoType + +from .const import _LOGGER, CONF_SERIAL, DOMAIN, TARGET_ANY +from .discovery import async_discover_devices +from .util import ( + async_entry_is_legacy, + async_execute_lifx, + async_get_legacy_entry, + formatted_serial, + lifx_features, + mac_matches_serial_number, +) -async def _async_has_devices(hass: HomeAssistant) -> bool: - """Return if there are devices that can be discovered.""" - lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() - return len(lifx_ip_addresses) > 0 +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for tplink.""" + VERSION = 1 -config_entry_flow.register_discovery_flow(DOMAIN, "LIFX", _async_has_devices) + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_devices: dict[str, Light] = {} + self._discovered_device: Light | None = None + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + mac = discovery_info.macaddress + host = discovery_info.ip + hass = self.hass + for entry in self._async_current_entries(): + if ( + entry.unique_id + and not async_entry_is_legacy(entry) + and mac_matches_serial_number(mac, entry.unique_id) + ): + if entry.data[CONF_HOST] != host: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_HOST: host} + ) + hass.async_create_task( + hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + return await self._async_handle_discovery(host) + + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle HomeKit discovery.""" + return await self._async_handle_discovery(host=discovery_info.host) + + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle discovery.""" + _LOGGER.debug("async_step_integration_discovery %s", discovery_info) + serial = discovery_info[CONF_SERIAL] + host = discovery_info[CONF_HOST] + await self.async_set_unique_id(formatted_serial(serial)) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return await self._async_handle_discovery(host, serial) + + async def _async_handle_discovery( + self, host: str, serial: str | None = None + ) -> FlowResult: + """Handle any discovery.""" + _LOGGER.debug("Discovery %s %s", host, serial) + self._async_abort_entries_match({CONF_HOST: host}) + self.context[CONF_HOST] = host + if any( + progress.get("context", {}).get(CONF_HOST) == host + for progress in self._async_in_progress() + ): + return self.async_abort(reason="already_in_progress") + if not ( + device := await self._async_try_connect( + host, serial=serial, raise_on_progress=True + ) + ): + return self.async_abort(reason="cannot_connect") + self._discovered_device = device + return await self.async_step_discovery_confirm() + + @callback + def _async_discovered_pending_migration(self) -> bool: + """Check if a discovered device is pending migration.""" + assert self.unique_id is not None + if not (legacy_entry := async_get_legacy_entry(self.hass)): + return False + device_registry = dr.async_get(self.hass) + existing_device = device_registry.async_get_device( + identifiers={(DOMAIN, self.unique_id)} + ) + return bool( + existing_device is not None + and legacy_entry.entry_id in existing_device.config_entries + ) + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + _LOGGER.debug( + "Confirming discovery: %s with serial %s", + self._discovered_device.label, + self.unique_id, + ) + if user_input is not None or self._async_discovered_pending_migration(): + return self._async_create_entry_from_device(self._discovered_device) + + self._set_confirm_only() + placeholders = { + "label": self._discovered_device.label, + "host": self._discovered_device.ip_addr, + "serial": self.unique_id, + } + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + host = user_input[CONF_HOST] + if not host: + return await self.async_step_pick_device() + if ( + device := await self._async_try_connect(host, raise_on_progress=False) + ) is None: + errors["base"] = "cannot_connect" + else: + return self._async_create_entry_from_device(device) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}), + errors=errors, + ) + + async def async_step_pick_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the step to pick discovered device.""" + if user_input is not None: + serial = user_input[CONF_DEVICE] + await self.async_set_unique_id(serial, raise_on_progress=False) + device_without_label = self._discovered_devices[serial] + device = await self._async_try_connect( + device_without_label.ip_addr, raise_on_progress=False + ) + if not device: + return self.async_abort(reason="cannot_connect") + return self._async_create_entry_from_device(device) + + configured_serials: set[str] = set() + configured_hosts: set[str] = set() + for entry in self._async_current_entries(): + if entry.unique_id and not async_entry_is_legacy(entry): + configured_serials.add(entry.unique_id) + configured_hosts.add(entry.data[CONF_HOST]) + self._discovered_devices = { + # device.mac_addr is not the mac_address, its the serial number + device.mac_addr: device + for device in await async_discover_devices(self.hass) + } + devices_name = { + serial: f"{serial} ({device.ip_addr})" + for serial, device in self._discovered_devices.items() + if serial not in configured_serials + and device.ip_addr not in configured_hosts + } + # Check if there is at least one device + if not devices_name: + return self.async_abort(reason="no_devices_found") + return self.async_show_form( + step_id="pick_device", + data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), + ) + + @callback + def _async_create_entry_from_device(self, device: Light) -> FlowResult: + """Create a config entry from a smart device.""" + self._abort_if_unique_id_configured(updates={CONF_HOST: device.ip_addr}) + return self.async_create_entry( + title=device.label, + data={CONF_HOST: device.ip_addr}, + ) + + async def _async_try_connect( + self, host: str, serial: str | None = None, raise_on_progress: bool = True + ) -> Light | None: + """Try to connect.""" + self._async_abort_entries_match({CONF_HOST: host}) + connection = LIFXConnection(host, TARGET_ANY) + try: + await connection.async_setup() + except socket.gaierror: + return None + device: Light = connection.device + device.get_hostfirmware() + try: + message = await async_execute_lifx(device.get_color) + except asyncio.TimeoutError: + return None + finally: + connection.async_stop() + if ( + lifx_features(device)["relays"] is True + or device.host_firmware_version is None + ): + return None # relays not supported + # device.mac_addr is not the mac_address, its the serial number + device.mac_addr = serial or message.target_addr + await self.async_set_unique_id( + formatted_serial(device.mac_addr), raise_on_progress=raise_on_progress + ) + return device diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index 8628527c428..ec756c2091f 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -1,3 +1,19 @@ """Const for LIFX.""" +import logging + DOMAIN = "lifx" + +TARGET_ANY = "00:00:00:00:00:00" + +DISCOVERY_INTERVAL = 10 +MESSAGE_TIMEOUT = 1.65 +MESSAGE_RETRIES = 5 +OVERALL_TIMEOUT = 9 +UNAVAILABLE_GRACE = 90 + +CONF_SERIAL = "serial" + +DATA_LIFX_MANAGER = "lifx_manager" + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py new file mode 100644 index 00000000000..87ba46e94d1 --- /dev/null +++ b/homeassistant/components/lifx/coordinator.py @@ -0,0 +1,158 @@ +"""Coordinator for lifx.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +from functools import partial +from typing import cast + +from aiolifx.aiolifx import Light +from aiolifx_connection import LIFXConnection + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + _LOGGER, + MESSAGE_RETRIES, + MESSAGE_TIMEOUT, + TARGET_ANY, + UNAVAILABLE_GRACE, +) +from .util import async_execute_lifx, get_real_mac_addr, lifx_features + +REQUEST_REFRESH_DELAY = 0.35 + + +class LIFXUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator to gather data for a specific lifx device.""" + + def __init__( + self, + hass: HomeAssistant, + connection: LIFXConnection, + title: str, + ) -> None: + """Initialize DataUpdateCoordinator.""" + assert connection.device is not None + self.connection = connection + self.device: Light = connection.device + self.lock = asyncio.Lock() + update_interval = timedelta(seconds=10) + super().__init__( + hass, + _LOGGER, + name=f"{title} ({self.device.ip_addr})", + update_interval=update_interval, + # We don't want an immediate refresh since the device + # takes a moment to reflect the state change + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), + ) + + @callback + def async_setup(self) -> None: + """Change timeouts.""" + self.device.timeout = MESSAGE_TIMEOUT + self.device.retry_count = MESSAGE_RETRIES + self.device.unregister_timeout = UNAVAILABLE_GRACE + + @property + def serial_number(self) -> str: + """Return the internal mac address.""" + return cast( + str, self.device.mac_addr + ) # device.mac_addr is not the mac_address, its the serial number + + @property + def mac_address(self) -> str: + """Return the physical mac address.""" + return get_real_mac_addr( + # device.mac_addr is not the mac_address, its the serial number + self.device.mac_addr, + self.device.host_firmware_version, + ) + + async def _async_update_data(self) -> None: + """Fetch all device data from the api.""" + async with self.lock: + if self.device.host_firmware_version is None: + self.device.get_hostfirmware() + if self.device.product is None: + self.device.get_version() + try: + response = await async_execute_lifx(self.device.get_color) + except asyncio.TimeoutError as ex: + raise UpdateFailed( + f"Failed to fetch state from device: {self.device.ip_addr}" + ) from ex + if self.device.product is None: + raise UpdateFailed( + f"Failed to fetch get version from device: {self.device.ip_addr}" + ) + # device.mac_addr is not the mac_address, its the serial number + if self.device.mac_addr == TARGET_ANY: + self.device.mac_addr = response.target_addr + if lifx_features(self.device)["multizone"]: + try: + await self.async_update_color_zones() + except asyncio.TimeoutError as ex: + raise UpdateFailed( + f"Failed to fetch zones from device: {self.device.ip_addr}" + ) from ex + + async def async_update_color_zones(self) -> None: + """Get updated color information for each zone.""" + zone = 0 + top = 1 + while zone < top: + # Each get_color_zones can update 8 zones at once + resp = await async_execute_lifx( + partial(self.device.get_color_zones, start_index=zone) + ) + zone += 8 + top = resp.count + + # We only await multizone responses so don't ask for just one + if zone == top - 1: + zone -= 1 + + async def async_get_color(self) -> None: + """Send a get color message to the device.""" + await async_execute_lifx(self.device.get_color) + + async def async_set_power(self, state: bool, duration: int | None) -> None: + """Send a set power message to the device.""" + await async_execute_lifx( + partial(self.device.set_power, state, duration=duration) + ) + + async def async_set_color( + self, hsbk: list[float | int | None], duration: int | None + ) -> None: + """Send a set color message to the device.""" + await async_execute_lifx( + partial(self.device.set_color, hsbk, duration=duration) + ) + + async def async_set_color_zones( + self, + start_index: int, + end_index: int, + hsbk: list[float | int | None], + duration: int | None, + apply: int, + ) -> None: + """Send a set color zones message to the device.""" + await async_execute_lifx( + partial( + self.device.set_color_zones, + start_index=start_index, + end_index=end_index, + color=hsbk, + duration=duration, + apply=apply, + ) + ) diff --git a/homeassistant/components/lifx/discovery.py b/homeassistant/components/lifx/discovery.py new file mode 100644 index 00000000000..1c6e9ab3060 --- /dev/null +++ b/homeassistant/components/lifx/discovery.py @@ -0,0 +1,58 @@ +"""The lifx integration discovery.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable + +from aiolifx.aiolifx import LifxDiscovery, Light, ScanManager + +from homeassistant import config_entries +from homeassistant.components import network +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant, callback + +from .const import CONF_SERIAL, DOMAIN + +DEFAULT_TIMEOUT = 8.5 + + +async def async_discover_devices(hass: HomeAssistant) -> Iterable[Light]: + """Discover lifx devices.""" + all_lights: dict[str, Light] = {} + broadcast_addrs = await network.async_get_ipv4_broadcast_addresses(hass) + discoveries = [] + for address in broadcast_addrs: + manager = ScanManager(str(address)) + lifx_discovery = LifxDiscovery(hass.loop, manager, broadcast_ip=str(address)) + discoveries.append(lifx_discovery) + lifx_discovery.start() + + await asyncio.sleep(DEFAULT_TIMEOUT) + for discovery in discoveries: + all_lights.update(discovery.lights) + discovery.cleanup() + + return all_lights.values() + + +@callback +def async_init_discovery_flow(hass: HomeAssistant, host: str, serial: str) -> None: + """Start discovery of devices.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_HOST: host, CONF_SERIAL: serial}, + ) + ) + + +@callback +def async_trigger_discovery( + hass: HomeAssistant, + discovered_devices: Iterable[Light], +) -> None: + """Trigger config flows for discovered devices.""" + for device in discovered_devices: + # device.mac_addr is not the mac_address, its the serial number + async_init_discovery_flow(hass, device.ip_addr, device.mac_addr) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 28390e5c02a..28a678d5e8f 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -2,86 +2,56 @@ from __future__ import annotations import asyncio -from dataclasses import dataclass -from datetime import timedelta -from functools import partial -from ipaddress import IPv4Address -import logging +from datetime import datetime, timedelta import math +from typing import Any -import aiolifx as aiolifx_module -from aiolifx.aiolifx import LifxDiscovery, Light +from aiolifx import products import aiolifx_effects as aiolifx_effects_module -from awesomeversion import AwesomeVersion import voluptuous as vol from homeassistant import util -from homeassistant.components import network from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_HS_COLOR, - ATTR_KELVIN, - ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_XY_COLOR, - COLOR_GROUP, - DOMAIN, LIGHT_TURN_ON_SCHEMA, - VALID_BRIGHTNESS, - VALID_BRIGHTNESS_PCT, ColorMode, LightEntity, LightEntityFeature, - preprocess_turn_on_alternatives, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_MODE, - ATTR_MODEL, - ATTR_SW_VERSION, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODEL, ATTR_SW_VERSION +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback, EntityPlatform -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.color as color_util -from . import ( - CONF_BROADCAST, - CONF_PORT, - CONF_SERVER, - DATA_LIFX_MANAGER, - DOMAIN as LIFX_DOMAIN, +from .const import DATA_LIFX_MANAGER, DOMAIN +from .coordinator import LIFXUpdateCoordinator +from .manager import ( + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + LIFXManager, ) - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=10) - -DISCOVERY_INTERVAL = 10 -MESSAGE_TIMEOUT = 1 -MESSAGE_RETRIES = 8 -UNAVAILABLE_GRACE = 90 - -FIX_MAC_FW = AwesomeVersion("3.70") +from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk SERVICE_LIFX_SET_STATE = "set_state" +COLOR_ZONE_POPULATE_DELAY = 0.3 + ATTR_INFRARED = "infrared" ATTR_ZONES = "zones" ATTR_POWER = "power" +SERVICE_LIFX_SET_STATE = "set_state" + LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( { **LIGHT_TURN_ON_SCHEMA, @@ -91,645 +61,152 @@ LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( } ) -SERVICE_EFFECT_PULSE = "effect_pulse" -SERVICE_EFFECT_COLORLOOP = "effect_colorloop" -SERVICE_EFFECT_STOP = "effect_stop" - -ATTR_POWER_ON = "power_on" -ATTR_PERIOD = "period" -ATTR_CYCLES = "cycles" -ATTR_SPREAD = "spread" -ATTR_CHANGE = "change" - -PULSE_MODE_BLINK = "blink" -PULSE_MODE_BREATHE = "breathe" -PULSE_MODE_PING = "ping" -PULSE_MODE_STROBE = "strobe" -PULSE_MODE_SOLID = "solid" - -PULSE_MODES = [ - PULSE_MODE_BLINK, - PULSE_MODE_BREATHE, - PULSE_MODE_PING, - PULSE_MODE_STROBE, - PULSE_MODE_SOLID, -] - -LIFX_EFFECT_SCHEMA = { - vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, -} - -LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( - { - **LIFX_EFFECT_SCHEMA, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, - vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) - ), - vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) - ), - vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( - vol.Coerce(tuple), - vol.ExactSequence( - ( - vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), - ) - ), - ), - vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, - ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), - ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), - ATTR_MODE: vol.In(PULSE_MODES), - } -) - -LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema( - { - **LIFX_EFFECT_SCHEMA, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), - ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), - ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), - ATTR_TRANSITION: cv.positive_float, - } -) - -LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({}) - - -def aiolifx(): - """Return the aiolifx module.""" - return aiolifx_module - - -def aiolifx_effects(): - """Return the aiolifx_effects module.""" - return aiolifx_effects_module - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the LIFX light platform. Obsolete.""" - _LOGGER.warning("LIFX no longer works with light platform configuration") +HSBK_HUE = 0 +HSBK_SATURATION = 1 +HSBK_BRIGHTNESS = 2 +HSBK_KELVIN = 3 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up LIFX from a config entry.""" - # Priority 1: manual config - if not (interfaces := hass.data[LIFX_DOMAIN].get(DOMAIN)): - # Priority 2: Home Assistant enabled interfaces - ip_addresses = ( - source_ip - for source_ip in await network.async_get_enabled_source_ips(hass) - if isinstance(source_ip, IPv4Address) and not source_ip.is_loopback - ) - interfaces = [{CONF_SERVER: str(ip)} for ip in ip_addresses] - + domain_data = hass.data[DOMAIN] + coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id] + manager: LIFXManager = domain_data[DATA_LIFX_MANAGER] + device = coordinator.device platform = entity_platform.async_get_current_platform() - lifx_manager = LIFXManager(hass, platform, config_entry, async_add_entities) - hass.data[DATA_LIFX_MANAGER] = lifx_manager - - for interface in interfaces: - lifx_manager.start_discovery(interface) - - -def lifx_features(bulb): - """Return a feature map for this bulb, or a default map if unknown.""" - return aiolifx().products.features_map.get( - bulb.product - ) or aiolifx().products.features_map.get(1) - - -def find_hsbk(hass, **kwargs): - """Find the desired color from a number of possible inputs.""" - hue, saturation, brightness, kelvin = [None] * 4 - - preprocess_turn_on_alternatives(hass, kwargs) - - if ATTR_HS_COLOR in kwargs: - hue, saturation = kwargs[ATTR_HS_COLOR] - elif ATTR_RGB_COLOR in kwargs: - hue, saturation = color_util.color_RGB_to_hs(*kwargs[ATTR_RGB_COLOR]) - elif ATTR_XY_COLOR in kwargs: - hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) - - if hue is not None: - hue = int(hue / 360 * 65535) - saturation = int(saturation / 100 * 65535) - kelvin = 3500 - - if ATTR_COLOR_TEMP in kwargs: - kelvin = int( - color_util.color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - ) - saturation = 0 - - if ATTR_BRIGHTNESS in kwargs: - brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) - - hsbk = [hue, saturation, brightness, kelvin] - return None if hsbk == [None] * 4 else hsbk - - -def merge_hsbk(base, change): - """Copy change on top of base, except when None.""" - if change is None: - return None - return [b if c is None else c for b, c in zip(base, change)] - - -@dataclass -class InFlightDiscovery: - """Represent a LIFX device that is being discovered.""" - - device: Light - lock: asyncio.Lock - - -class LIFXManager: - """Representation of all known LIFX entities.""" - - def __init__( - self, - hass: HomeAssistant, - platform: EntityPlatform, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - ) -> None: - """Initialize the light.""" - self.entities: dict[str, LIFXLight] = {} - self.switch_devices: list[str] = [] - self.hass = hass - self.platform = platform - self.config_entry = config_entry - self.async_add_entities = async_add_entities - self.effects_conductor = aiolifx_effects().Conductor(hass.loop) - self.discoveries: list[LifxDiscovery] = [] - self.discoveries_inflight: dict[str, InFlightDiscovery] = {} - self.cleanup_unsub = self.hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, self.cleanup - ) - self.entity_registry_updated_unsub = self.hass.bus.async_listen( - er.EVENT_ENTITY_REGISTRY_UPDATED, self.entity_registry_updated - ) - - self.register_set_state() - self.register_effects() - - def start_discovery(self, interface): - """Start discovery on a network interface.""" - kwargs = {"discovery_interval": DISCOVERY_INTERVAL} - if broadcast_ip := interface.get(CONF_BROADCAST): - kwargs["broadcast_ip"] = broadcast_ip - lifx_discovery = aiolifx().LifxDiscovery(self.hass.loop, self, **kwargs) - - kwargs = {} - if listen_ip := interface.get(CONF_SERVER): - kwargs["listen_ip"] = listen_ip - if listen_port := interface.get(CONF_PORT): - kwargs["listen_port"] = listen_port - lifx_discovery.start(**kwargs) - - self.discoveries.append(lifx_discovery) - - @callback - def cleanup(self, event=None): - """Release resources.""" - self.cleanup_unsub() - self.entity_registry_updated_unsub() - - for discovery in self.discoveries: - discovery.cleanup() - - for service in ( - SERVICE_LIFX_SET_STATE, - SERVICE_EFFECT_STOP, - SERVICE_EFFECT_PULSE, - SERVICE_EFFECT_COLORLOOP, - ): - self.hass.services.async_remove(LIFX_DOMAIN, service) - - def register_set_state(self): - """Register the LIFX set_state service call.""" - self.platform.async_register_entity_service( - SERVICE_LIFX_SET_STATE, LIFX_SET_STATE_SCHEMA, "set_state" - ) - - def register_effects(self): - """Register the LIFX effects as hass service calls.""" - - async def service_handler(service: ServiceCall) -> None: - """Apply a service, i.e. start an effect.""" - entities = await self.platform.async_extract_from_service(service) - if entities: - await self.start_effect(entities, service.service, **service.data) - - self.hass.services.async_register( - LIFX_DOMAIN, - SERVICE_EFFECT_PULSE, - service_handler, - schema=LIFX_EFFECT_PULSE_SCHEMA, - ) - - self.hass.services.async_register( - LIFX_DOMAIN, - SERVICE_EFFECT_COLORLOOP, - service_handler, - schema=LIFX_EFFECT_COLORLOOP_SCHEMA, - ) - - self.hass.services.async_register( - LIFX_DOMAIN, - SERVICE_EFFECT_STOP, - service_handler, - schema=LIFX_EFFECT_STOP_SCHEMA, - ) - - async def start_effect(self, entities, service, **kwargs): - """Start a light effect on entities.""" - bulbs = [light.bulb for light in entities] - - if service == SERVICE_EFFECT_PULSE: - effect = aiolifx_effects().EffectPulse( - power_on=kwargs.get(ATTR_POWER_ON), - period=kwargs.get(ATTR_PERIOD), - cycles=kwargs.get(ATTR_CYCLES), - mode=kwargs.get(ATTR_MODE), - hsbk=find_hsbk(self.hass, **kwargs), - ) - await self.effects_conductor.start(effect, bulbs) - elif service == SERVICE_EFFECT_COLORLOOP: - preprocess_turn_on_alternatives(self.hass, kwargs) - - brightness = None - if ATTR_BRIGHTNESS in kwargs: - brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) - - effect = aiolifx_effects().EffectColorloop( - power_on=kwargs.get(ATTR_POWER_ON), - period=kwargs.get(ATTR_PERIOD), - change=kwargs.get(ATTR_CHANGE), - spread=kwargs.get(ATTR_SPREAD), - transition=kwargs.get(ATTR_TRANSITION), - brightness=brightness, - ) - await self.effects_conductor.start(effect, bulbs) - elif service == SERVICE_EFFECT_STOP: - await self.effects_conductor.stop(bulbs) - - def clear_inflight_discovery(self, inflight: InFlightDiscovery) -> None: - """Clear in-flight discovery.""" - self.discoveries_inflight.pop(inflight.device.mac_addr, None) - - @callback - def register(self, bulb: Light) -> None: - """Allow a single in-flight discovery per bulb.""" - if bulb.mac_addr in self.switch_devices: - _LOGGER.debug( - "Skipping discovered LIFX Switch at %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - return - - # Try to bail out of discovery as early as possible - if bulb.mac_addr in self.entities: - entity = self.entities[bulb.mac_addr] - entity.registered = True - _LOGGER.debug("Reconnected to %s", entity.who) - return - - if bulb.mac_addr not in self.discoveries_inflight: - inflight = InFlightDiscovery(bulb, asyncio.Lock()) - self.discoveries_inflight[bulb.mac_addr] = inflight - _LOGGER.debug( - "First discovery response received from %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - else: - _LOGGER.debug( - "Duplicate discovery response received from %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - - self.hass.async_create_task( - self._async_handle_discovery(self.discoveries_inflight[bulb.mac_addr]) - ) - - async def _async_handle_discovery(self, inflight: InFlightDiscovery) -> None: - """Handle LIFX bulb registration lifecycle.""" - - # only allow a single discovery process per discovered device - async with inflight.lock: - - # Bail out if an entity was created by a previous discovery while - # this discovery was waiting for the asyncio lock to release. - if inflight.device.mac_addr in self.entities: - self.clear_inflight_discovery(inflight) - entity: LIFXLight = self.entities[inflight.device.mac_addr] - entity.registered = True - _LOGGER.debug("Reconnected to %s", entity.who) - return - - # Determine the product info so that LIFX Switches - # can be skipped. - ack = AwaitAioLIFX().wait - - if inflight.device.product is None: - if await ack(inflight.device.get_version) is None: - _LOGGER.debug( - "Failed to discover product information for %s (%s)", - inflight.device.ip_addr, - inflight.device.mac_addr, - ) - self.clear_inflight_discovery(inflight) - return - - if lifx_features(inflight.device)["relays"] is True: - _LOGGER.debug( - "Skipping discovered LIFX Switch at %s (%s)", - inflight.device.ip_addr, - inflight.device.mac_addr, - ) - self.switch_devices.append(inflight.device.mac_addr) - self.clear_inflight_discovery(inflight) - return - - await self._async_process_discovery(inflight=inflight) - - async def _async_process_discovery(self, inflight: InFlightDiscovery) -> None: - """Process discovery of a device.""" - bulb = inflight.device - ack = AwaitAioLIFX().wait - - bulb.timeout = MESSAGE_TIMEOUT - bulb.retry_count = MESSAGE_RETRIES - bulb.unregister_timeout = UNAVAILABLE_GRACE - - # Read initial state - if bulb.color is None: - if await ack(bulb.get_color) is None: - _LOGGER.debug( - "Failed to determine current state of %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - self.clear_inflight_discovery(inflight) - return - - if lifx_features(bulb)["multizone"]: - entity: LIFXLight = LIFXStrip(bulb.mac_addr, bulb, self.effects_conductor) - elif lifx_features(bulb)["color"]: - entity = LIFXColor(bulb.mac_addr, bulb, self.effects_conductor) - else: - entity = LIFXWhite(bulb.mac_addr, bulb, self.effects_conductor) - - self.entities[bulb.mac_addr] = entity - self.async_add_entities([entity], True) - _LOGGER.debug("Entity created for %s", entity.who) - self.clear_inflight_discovery(inflight) - - @callback - def unregister(self, bulb: Light) -> None: - """Mark unresponsive bulbs as unavailable in Home Assistant.""" - if bulb.mac_addr in self.entities: - entity = self.entities[bulb.mac_addr] - entity.registered = False - entity.async_write_ha_state() - _LOGGER.debug("Disconnected from %s", entity.who) - - @callback - def entity_registry_updated(self, event): - """Handle entity registry updated.""" - if event.data["action"] == "remove": - self.remove_empty_devices() - - def remove_empty_devices(self): - """Remove devices with no entities.""" - entity_reg = er.async_get(self.hass) - device_reg = dr.async_get(self.hass) - device_list = dr.async_entries_for_config_entry( - device_reg, self.config_entry.entry_id - ) - for device_entry in device_list: - if not er.async_entries_for_device( - entity_reg, - device_entry.id, - include_disabled_entities=True, - ): - device_reg.async_update_device( - device_entry.id, remove_config_entry_id=self.config_entry.entry_id - ) - - -class AwaitAioLIFX: - """Wait for an aiolifx callback and return the message.""" - - def __init__(self): - """Initialize the wrapper.""" - self.message = None - self.event = asyncio.Event() - - @callback - def callback(self, bulb, message): - """Handle responses.""" - self.message = message - self.event.set() - - async def wait(self, method): - """Call an aiolifx method and wait for its response.""" - self.message = None - self.event.clear() - method(callb=self.callback) - - await self.event.wait() - return self.message - - -def convert_8_to_16(value): - """Scale an 8 bit level into 16 bits.""" - return (value << 8) | value - - -def convert_16_to_8(value): - """Scale a 16 bit level into 8 bits.""" - return value >> 8 - - -class LIFXLight(LightEntity): + platform.async_register_entity_service( + SERVICE_LIFX_SET_STATE, + LIFX_SET_STATE_SCHEMA, + "set_state", + ) + if lifx_features(device)["multizone"]: + entity: LIFXLight = LIFXStrip(coordinator, manager, entry) + elif lifx_features(device)["color"]: + entity = LIFXColor(coordinator, manager, entry) + else: + entity = LIFXWhite(coordinator, manager, entry) + async_add_entities([entity]) + + +class LIFXLight(CoordinatorEntity[LIFXUpdateCoordinator], LightEntity): """Representation of a LIFX light.""" _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT def __init__( self, - mac_addr: str, - bulb: Light, - effects_conductor: aiolifx_effects_module.Conductor, + coordinator: LIFXUpdateCoordinator, + manager: LIFXManager, + entry: ConfigEntry, ) -> None: """Initialize the light.""" - self.mac_addr = mac_addr + super().__init__(coordinator) + bulb = coordinator.device + self.mac_addr = bulb.mac_addr self.bulb = bulb - self.effects_conductor = effects_conductor - self.registered = True - self.postponed_update = None - self.lock = asyncio.Lock() - - def get_mac_addr(self): - """Increment the last byte of the mac address by one for FW>3.70.""" - if ( - self.bulb.host_firmware_version - and AwesomeVersion(self.bulb.host_firmware_version) >= FIX_MAC_FW - ): - octets = [int(octet, 16) for octet in self.mac_addr.split(":")] - octets[5] = (octets[5] + 1) % 256 - return ":".join(f"{octet:02x}" for octet in octets) - return self.mac_addr - - @property - def device_info(self) -> DeviceInfo: - """Return information about the device.""" - _map = aiolifx().products.product_map - + bulb_features = lifx_features(bulb) + self.manager = manager + self.effects_conductor: aiolifx_effects_module.Conductor = ( + manager.effects_conductor + ) + self.postponed_update: CALLBACK_TYPE | None = None + self.entry = entry + self._attr_unique_id = self.coordinator.serial_number + self._attr_name = bulb.label + self._attr_min_mireds = math.floor( + color_util.color_temperature_kelvin_to_mired(bulb_features["max_kelvin"]) + ) + self._attr_max_mireds = math.ceil( + color_util.color_temperature_kelvin_to_mired(bulb_features["min_kelvin"]) + ) info = DeviceInfo( - identifiers={(LIFX_DOMAIN, self.unique_id)}, - connections={(dr.CONNECTION_NETWORK_MAC, self.get_mac_addr())}, + identifiers={(DOMAIN, coordinator.serial_number)}, + connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)}, manufacturer="LIFX", name=self.name, ) - - if (model := (_map.get(self.bulb.product) or self.bulb.product)) is not None: + _map = products.product_map + if (model := (_map.get(bulb.product) or bulb.product)) is not None: info[ATTR_MODEL] = str(model) - if (version := self.bulb.host_firmware_version) is not None: + if (version := bulb.host_firmware_version) is not None: info[ATTR_SW_VERSION] = version - - return info - - @property - def available(self): - """Return the availability of the bulb.""" - return self.registered - - @property - def unique_id(self): - """Return a unique ID.""" - return self.mac_addr - - @property - def name(self): - """Return the name of the bulb.""" - return self.bulb.label - - @property - def who(self): - """Return a string identifying the bulb by name and mac.""" - return f"{self.name} ({self.mac_addr})" - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - kelvin = lifx_features(self.bulb)["max_kelvin"] - return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin)) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - kelvin = lifx_features(self.bulb)["min_kelvin"] - return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin)) - - @property - def color_mode(self) -> ColorMode: - """Return the color mode of the light.""" - bulb_features = lifx_features(self.bulb) + self._attr_device_info = info if bulb_features["min_kelvin"] != bulb_features["max_kelvin"]: - return ColorMode.COLOR_TEMP - return ColorMode.BRIGHTNESS + color_mode = ColorMode.COLOR_TEMP + else: + color_mode = ColorMode.BRIGHTNESS + self._attr_color_mode = color_mode + self._attr_supported_color_modes = {color_mode} @property - def supported_color_modes(self) -> set[ColorMode]: - """Flag supported color modes.""" - return {self.color_mode} - - @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" fade = self.bulb.power_level / 65535 - return convert_16_to_8(int(fade * self.bulb.color[2])) + return convert_16_to_8(int(fade * self.bulb.color[HSBK_BRIGHTNESS])) @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the color temperature.""" - _, sat, _, kelvin = self.bulb.color - if sat: - return None - return color_util.color_temperature_kelvin_to_mired(kelvin) + return color_util.color_temperature_kelvin_to_mired( + self.bulb.color[HSBK_KELVIN] + ) @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" - return self.bulb.power_level != 0 + return bool(self.bulb.power_level != 0) @property - def effect(self): + def effect(self) -> str | None: """Return the name of the currently running effect.""" - effect = self.effects_conductor.effect(self.bulb) - if effect: - return f"lifx_effect_{effect.name}" + if effect := self.effects_conductor.effect(self.bulb): + return f"effect_{effect.name}" return None - async def update_hass(self, now=None): - """Request new status and push it to hass.""" - self.postponed_update = None - await self.async_update() - self.async_write_ha_state() - - async def update_during_transition(self, when): + async def update_during_transition(self, when: int) -> None: """Update state at the start and end of a transition.""" if self.postponed_update: self.postponed_update() + self.postponed_update = None # Transition has started - await self.update_hass() + self.async_write_ha_state() + + # The state reply we get back may be stale so we also request + # a refresh to get a fresh state + # https://lan.developer.lifx.com/docs/changing-a-device + await self.coordinator.async_request_refresh() # Transition has ended if when > 0: + + async def _async_refresh(now: datetime) -> None: + """Refresh the state.""" + await self.coordinator.async_refresh() + self.postponed_update = async_track_point_in_utc_time( self.hass, - self.update_hass, + _async_refresh, util.dt.utcnow() + timedelta(milliseconds=when), ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - kwargs[ATTR_POWER] = True - self.hass.async_create_task(self.set_state(**kwargs)) + await self.set_state(**{**kwargs, ATTR_POWER: True}) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - kwargs[ATTR_POWER] = False - self.hass.async_create_task(self.set_state(**kwargs)) + await self.set_state(**{**kwargs, ATTR_POWER: False}) - async def set_state(self, **kwargs): + async def set_state(self, **kwargs: Any) -> None: """Set a color on the light and turn it on/off.""" - async with self.lock: + self.coordinator.async_set_updated_data(None) + async with self.coordinator.lock: + # Cancel any pending refreshes bulb = self.bulb await self.effects_conductor.stop([bulb]) @@ -752,89 +229,113 @@ class LIFXLight(LightEntity): hsbk = find_hsbk(self.hass, **kwargs) - # Send messages, waiting for ACK each time - ack = AwaitAioLIFX().wait - if not self.is_on: if power_off: - await self.set_power(ack, False) + await self.set_power(False) # If fading on with color, set color immediately if hsbk and power_on: - await self.set_color(ack, hsbk, kwargs) - await self.set_power(ack, True, duration=fade) + await self.set_color(hsbk, kwargs) + await self.set_power(True, duration=fade) elif hsbk: - await self.set_color(ack, hsbk, kwargs, duration=fade) + await self.set_color(hsbk, kwargs, duration=fade) elif power_on: - await self.set_power(ack, True, duration=fade) + await self.set_power(True, duration=fade) else: - if power_on: - await self.set_power(ack, True) if hsbk: - await self.set_color(ack, hsbk, kwargs, duration=fade) + await self.set_color(hsbk, kwargs, duration=fade) + # The response from set_color will tell us if the + # bulb is actually on or not, so we don't need to + # call power_on if its already on + if power_on and self.bulb.power_level == 0: + await self.set_power(True) + elif power_on: + await self.set_power(True) if power_off: - await self.set_power(ack, False, duration=fade) - - # Avoid state ping-pong by holding off updates as the state settles - await asyncio.sleep(0.3) + await self.set_power(False, duration=fade) # Update when the transition starts and ends await self.update_during_transition(fade) - async def set_power(self, ack, pwr, duration=0): + async def set_power( + self, + pwr: bool, + duration: int = 0, + ) -> None: """Send a power change to the bulb.""" - await ack(partial(self.bulb.set_power, pwr, duration=duration)) + try: + await self.coordinator.async_set_power(pwr, duration) + except asyncio.TimeoutError as ex: + raise HomeAssistantError(f"Timeout setting power for {self.name}") from ex - async def set_color(self, ack, hsbk, kwargs, duration=0): + async def set_color( + self, + hsbk: list[float | int | None], + kwargs: dict[str, Any], + duration: int = 0, + ) -> None: """Send a color change to the bulb.""" - hsbk = merge_hsbk(self.bulb.color, hsbk) - await ack(partial(self.bulb.set_color, hsbk, duration=duration)) + merged_hsbk = merge_hsbk(self.bulb.color, hsbk) + try: + await self.coordinator.async_set_color(merged_hsbk, duration) + except asyncio.TimeoutError as ex: + raise HomeAssistantError(f"Timeout setting color for {self.name}") from ex - async def default_effect(self, **kwargs): + async def get_color( + self, + ) -> None: + """Send a get color message to the bulb.""" + try: + await self.coordinator.async_get_color() + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting getting color for {self.name}" + ) from ex + + async def default_effect(self, **kwargs: Any) -> None: """Start an effect with default parameters.""" - service = kwargs[ATTR_EFFECT] - data = {ATTR_ENTITY_ID: self.entity_id} await self.hass.services.async_call( - LIFX_DOMAIN, service, data, context=self._context + DOMAIN, + kwargs[ATTR_EFFECT], + {ATTR_ENTITY_ID: self.entity_id}, + context=self._context, ) - async def async_update(self): - """Update bulb status.""" - if self.available and not self.lock.locked(): - await AwaitAioLIFX().wait(self.bulb.get_color) + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + self.manager.async_register_entity(self.entity_id, self.entry.entry_id) + ) + return await super().async_added_to_hass() class LIFXWhite(LIFXLight): """Representation of a white-only LIFX light.""" - @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return [SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP] + _attr_effect_list = [SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP] class LIFXColor(LIFXLight): """Representation of a color LIFX light.""" - @property - def color_mode(self) -> ColorMode: - """Return the color mode of the light.""" - sat = self.bulb.color[1] - if sat: - return ColorMode.HS - return ColorMode.COLOR_TEMP + _attr_effect_list = [ + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + ] @property def supported_color_modes(self) -> set[ColorMode]: - """Flag supported color modes.""" + """Return the supported color modes.""" return {ColorMode.COLOR_TEMP, ColorMode.HS} @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return [SERVICE_EFFECT_COLORLOOP, SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP] + def color_mode(self) -> ColorMode: + """Return the color mode of the light.""" + has_sat = self.bulb.color[HSBK_SATURATION] + return ColorMode.HS if has_sat else ColorMode.COLOR_TEMP @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the hs value.""" hue, sat, _, _ = self.bulb.color hue = hue / 65535 * 360 @@ -845,63 +346,70 @@ class LIFXColor(LIFXLight): class LIFXStrip(LIFXColor): """Representation of a LIFX light strip with multiple zones.""" - async def set_color(self, ack, hsbk, kwargs, duration=0): + async def set_color( + self, + hsbk: list[float | int | None], + kwargs: dict[str, Any], + duration: int = 0, + ) -> None: """Send a color change to the bulb.""" bulb = self.bulb - num_zones = len(bulb.color_zones) + color_zones = bulb.color_zones + num_zones = len(color_zones) + + # Zone brightness is not reported when powered off + if not self.is_on and hsbk[HSBK_BRIGHTNESS] is None: + await self.set_power(True) + await asyncio.sleep(COLOR_ZONE_POPULATE_DELAY) + await self.update_color_zones() + await self.set_power(False) if (zones := kwargs.get(ATTR_ZONES)) is None: # Fast track: setting all zones to the same brightness and color # can be treated as a single-zone bulb. - if hsbk[2] is not None and hsbk[3] is not None: - await super().set_color(ack, hsbk, kwargs, duration) + first_zone = color_zones[0] + first_zone_brightness = first_zone[HSBK_BRIGHTNESS] + all_zones_have_same_brightness = all( + color_zones[zone][HSBK_BRIGHTNESS] == first_zone_brightness + for zone in range(num_zones) + ) + all_zones_are_the_same = all( + color_zones[zone] == first_zone for zone in range(num_zones) + ) + if ( + all_zones_have_same_brightness or hsbk[HSBK_BRIGHTNESS] is not None + ) and (all_zones_are_the_same or hsbk[HSBK_KELVIN] is not None): + await super().set_color(hsbk, kwargs, duration) return zones = list(range(0, num_zones)) else: zones = [x for x in set(zones) if x < num_zones] - # Zone brightness is not reported when powered off - if not self.is_on and hsbk[2] is None: - await self.set_power(ack, True) - await asyncio.sleep(0.3) - await self.update_color_zones() - await self.set_power(ack, False) - await asyncio.sleep(0.3) - # Send new color to each zone for index, zone in enumerate(zones): - zone_hsbk = merge_hsbk(bulb.color_zones[zone], hsbk) + zone_hsbk = merge_hsbk(color_zones[zone], hsbk) apply = 1 if (index == len(zones) - 1) else 0 - set_zone = partial( - bulb.set_color_zones, - start_index=zone, - end_index=zone, - color=zone_hsbk, - duration=duration, - apply=apply, - ) - await ack(set_zone) + try: + await self.coordinator.async_set_color_zones( + zone, zone, zone_hsbk, duration, apply + ) + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting color zones for {self.name}" + ) from ex - async def async_update(self): - """Update strip status.""" - if self.available and not self.lock.locked(): - await super().async_update() - await self.update_color_zones() + # set_color_zones does not update the + # state of the bulb, so we need to do that + await self.get_color() - async def update_color_zones(self): - """Get updated color information for each zone.""" - zone = 0 - top = 1 - while self.available and zone < top: - # Each get_color_zones can update 8 zones at once - resp = await AwaitAioLIFX().wait( - partial(self.bulb.get_color_zones, start_index=zone) - ) - if resp: - zone += 8 - top = resp.count - - # We only await multizone responses so don't ask for just one - if zone == top - 1: - zone -= 1 + async def update_color_zones( + self, + ) -> None: + """Send a get color zones message to the bulb.""" + try: + await self.coordinator.async_update_color_zones() + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting updating color zones for {self.name}" + ) from ex diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py new file mode 100644 index 00000000000..ee5428e36a8 --- /dev/null +++ b/homeassistant/components/lifx/manager.py @@ -0,0 +1,216 @@ +"""Support for LIFX lights.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +from typing import Any + +import aiolifx_effects +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_GROUP, + VALID_BRIGHTNESS, + VALID_BRIGHTNESS_PCT, + preprocess_turn_on_alternatives, +) +from homeassistant.const import ATTR_MODE +from homeassistant.core import HomeAssistant, ServiceCall, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_extract_referenced_entity_ids + +from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN +from .util import convert_8_to_16, find_hsbk + +SCAN_INTERVAL = timedelta(seconds=10) + + +SERVICE_EFFECT_PULSE = "effect_pulse" +SERVICE_EFFECT_COLORLOOP = "effect_colorloop" +SERVICE_EFFECT_STOP = "effect_stop" + +ATTR_POWER_ON = "power_on" +ATTR_PERIOD = "period" +ATTR_CYCLES = "cycles" +ATTR_SPREAD = "spread" +ATTR_CHANGE = "change" + +PULSE_MODE_BLINK = "blink" +PULSE_MODE_BREATHE = "breathe" +PULSE_MODE_PING = "ping" +PULSE_MODE_STROBE = "strobe" +PULSE_MODE_SOLID = "solid" + +PULSE_MODES = [ + PULSE_MODE_BLINK, + PULSE_MODE_BREATHE, + PULSE_MODE_PING, + PULSE_MODE_STROBE, + PULSE_MODE_SOLID, +] + +LIFX_EFFECT_SCHEMA = { + vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, +} + +LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) + ), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) + ), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + ), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), + ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), + ATTR_MODE: vol.In(PULSE_MODES), + } +) + +LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), + ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_TRANSITION: cv.positive_float, + } +) + +LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({}) + +SERVICES = ( + SERVICE_EFFECT_STOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_COLORLOOP, +) + + +class LIFXManager: + """Representation of all known LIFX entities.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the manager.""" + self.hass = hass + self.effects_conductor = aiolifx_effects.Conductor(hass.loop) + self.entry_id_to_entity_id: dict[str, str] = {} + + @callback + def async_unload(self) -> None: + """Release resources.""" + for service in SERVICES: + self.hass.services.async_remove(DOMAIN, service) + + @callback + def async_register_entity( + self, entity_id: str, entry_id: str + ) -> Callable[[], None]: + """Register an entity to the config entry id.""" + self.entry_id_to_entity_id[entry_id] = entity_id + + @callback + def unregister_entity() -> None: + """Unregister entity when it is being destroyed.""" + self.entry_id_to_entity_id.pop(entry_id) + + return unregister_entity + + @callback + def async_setup(self) -> None: + """Register the LIFX effects as hass service calls.""" + + async def service_handler(service: ServiceCall) -> None: + """Apply a service, i.e. start an effect.""" + referenced = async_extract_referenced_entity_ids(self.hass, service) + all_referenced = referenced.referenced | referenced.indirectly_referenced + if all_referenced: + await self.start_effect(all_referenced, service.service, **service.data) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_PULSE, + service_handler, + schema=LIFX_EFFECT_PULSE_SCHEMA, + ) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_COLORLOOP, + service_handler, + schema=LIFX_EFFECT_COLORLOOP_SCHEMA, + ) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_STOP, + service_handler, + schema=LIFX_EFFECT_STOP_SCHEMA, + ) + + async def start_effect( + self, entity_ids: set[str], service: str, **kwargs: Any + ) -> None: + """Start a light effect on entities.""" + bulbs = [ + coordinator.device + for entry_id, coordinator in self.hass.data[DOMAIN].items() + if entry_id != DATA_LIFX_MANAGER + and self.entry_id_to_entity_id[entry_id] in entity_ids + ] + _LOGGER.debug("Starting effect %s on %s", service, bulbs) + + if service == SERVICE_EFFECT_PULSE: + effect = aiolifx_effects.EffectPulse( + power_on=kwargs.get(ATTR_POWER_ON), + period=kwargs.get(ATTR_PERIOD), + cycles=kwargs.get(ATTR_CYCLES), + mode=kwargs.get(ATTR_MODE), + hsbk=find_hsbk(self.hass, **kwargs), + ) + await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_COLORLOOP: + preprocess_turn_on_alternatives(self.hass, kwargs) # type: ignore[no-untyped-call] + + brightness = None + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + effect = aiolifx_effects.EffectColorloop( + power_on=kwargs.get(ATTR_POWER_ON), + period=kwargs.get(ATTR_PERIOD), + change=kwargs.get(ATTR_CHANGE), + spread=kwargs.get(ATTR_SPREAD), + transition=kwargs.get(ATTR_TRANSITION), + brightness=brightness, + ) + await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_STOP: + await self.effects_conductor.stop(bulbs) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 06e7b292ac6..ebc4d73ce5d 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,12 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.8.1", "aiolifx_effects==0.2.2"], + "requirements": [ + "aiolifx==0.8.1", + "aiolifx_effects==0.2.2", + "aiolifx-connection==1.0.0" + ], + "quality_scale": "platinum", "dependencies": ["network"], "homekit": { "models": [ @@ -29,7 +34,8 @@ "LIFX Z" ] }, - "codeowners": ["@Djelibeybi"], + "dhcp": [{ "macaddress": "D073D5*" }, { "registered_devices": true }], + "codeowners": ["@bdraco", "@Djelibeybi"], "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"] } diff --git a/homeassistant/components/lifx/migration.py b/homeassistant/components/lifx/migration.py new file mode 100644 index 00000000000..1ff94daa92f --- /dev/null +++ b/homeassistant/components/lifx/migration.py @@ -0,0 +1,74 @@ +"""Migrate lifx devices to their own config entry.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .const import _LOGGER, DOMAIN +from .discovery import async_init_discovery_flow + + +async def async_migrate_legacy_entries( + hass: HomeAssistant, + discovered_hosts_by_serial: dict[str, str], + existing_serials: set[str], + legacy_entry: ConfigEntry, +) -> int: + """Migrate the legacy config entries to have an entry per device.""" + _LOGGER.debug( + "Migrating legacy entries: discovered_hosts_by_serial=%s, existing_serials=%s", + discovered_hosts_by_serial, + existing_serials, + ) + + device_registry = dr.async_get(hass) + for dev_entry in dr.async_entries_for_config_entry( + device_registry, legacy_entry.entry_id + ): + for domain, serial in dev_entry.identifiers: + if ( + domain == DOMAIN + and serial not in existing_serials + and (host := discovered_hosts_by_serial.get(serial)) + ): + async_init_discovery_flow(hass, host, serial) + + remaining_devices = dr.async_entries_for_config_entry( + dr.async_get(hass), legacy_entry.entry_id + ) + _LOGGER.debug("The following devices remain: %s", remaining_devices) + return len(remaining_devices) + + +async def async_migrate_entities_devices( + hass: HomeAssistant, legacy_entry_id: str, new_entry: ConfigEntry +) -> None: + """Move entities and devices to the new config entry.""" + migrated_devices = [] + device_registry = dr.async_get(hass) + for dev_entry in dr.async_entries_for_config_entry( + device_registry, legacy_entry_id + ): + for domain, value in dev_entry.identifiers: + if domain == DOMAIN and value == new_entry.unique_id: + _LOGGER.debug( + "Migrating device with %s to %s", + dev_entry.identifiers, + new_entry.unique_id, + ) + migrated_devices.append(dev_entry.id) + device_registry.async_update_device( + dev_entry.id, + add_config_entry_id=new_entry.entry_id, + remove_config_entry_id=legacy_entry_id, + ) + + entity_registry = er.async_get(hass) + for reg_entity in er.async_entries_for_config_entry( + entity_registry, legacy_entry_id + ): + if reg_entity.device_id in migrated_devices: + entity_registry.async_update_entity( + reg_entity.entity_id, config_entry_id=new_entry.entry_id + ) diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json index ebb8b39a8bc..b83ae9c1609 100644 --- a/homeassistant/components/lifx/strings.json +++ b/homeassistant/components/lifx/strings.json @@ -1,12 +1,28 @@ { "config": { + "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Do you want to set up LIFX?" + "user": { + "description": "If you leave the host empty, discovery will be used to find devices.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "pick_device": { + "data": { + "device": "Device" + } + }, + "discovery_confirm": { + "description": "Do you want to setup {label} ({host}) {serial}?" } }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } } diff --git a/homeassistant/components/lifx/translations/en.json b/homeassistant/components/lifx/translations/en.json index 154101995ac..119259457a7 100644 --- a/homeassistant/components/lifx/translations/en.json +++ b/homeassistant/components/lifx/translations/en.json @@ -1,12 +1,28 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" }, + "error": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Do you want to set up LIFX?" + "discovery_confirm": { + "description": "Do you want to setup {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Device" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "If you leave the host empty, discovery will be used to find devices." } } } diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py new file mode 100644 index 00000000000..1de8bdae76a --- /dev/null +++ b/homeassistant/components/lifx/util.py @@ -0,0 +1,161 @@ +"""Support for LIFX.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from typing import Any + +from aiolifx import products +from aiolifx.aiolifx import Light +from aiolifx.message import Message +import async_timeout +from awesomeversion import AwesomeVersion + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, + preprocess_turn_on_alternatives, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +import homeassistant.util.color as color_util + +from .const import _LOGGER, DOMAIN, OVERALL_TIMEOUT + +FIX_MAC_FW = AwesomeVersion("3.70") + + +@callback +def async_entry_is_legacy(entry: ConfigEntry) -> bool: + """Check if a config entry is the legacy shared one.""" + return entry.unique_id is None or entry.unique_id == DOMAIN + + +@callback +def async_get_legacy_entry(hass: HomeAssistant) -> ConfigEntry | None: + """Get the legacy config entry.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if async_entry_is_legacy(entry): + return entry + return None + + +def convert_8_to_16(value: int) -> int: + """Scale an 8 bit level into 16 bits.""" + return (value << 8) | value + + +def convert_16_to_8(value: int) -> int: + """Scale a 16 bit level into 8 bits.""" + return value >> 8 + + +def lifx_features(bulb: Light) -> dict[str, Any]: + """Return a feature map for this bulb, or a default map if unknown.""" + features: dict[str, Any] = ( + products.features_map.get(bulb.product) or products.features_map[1] + ) + return features + + +def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | None: + """Find the desired color from a number of possible inputs. + + Hue, Saturation, Brightness, Kelvin + """ + hue, saturation, brightness, kelvin = [None] * 4 + + preprocess_turn_on_alternatives(hass, kwargs) # type: ignore[no-untyped-call] + + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] + elif ATTR_RGB_COLOR in kwargs: + hue, saturation = color_util.color_RGB_to_hs(*kwargs[ATTR_RGB_COLOR]) + elif ATTR_XY_COLOR in kwargs: + hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) + + if hue is not None: + assert saturation is not None + hue = int(hue / 360 * 65535) + saturation = int(saturation / 100 * 65535) + kelvin = 3500 + + if ATTR_COLOR_TEMP in kwargs: + kelvin = int( + color_util.color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + ) + saturation = 0 + + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + hsbk = [hue, saturation, brightness, kelvin] + return None if hsbk == [None] * 4 else hsbk + + +def merge_hsbk( + base: list[float | int | None], change: list[float | int | None] +) -> list[float | int | None]: + """Copy change on top of base, except when None. + + Hue, Saturation, Brightness, Kelvin + """ + return [b if c is None else c for b, c in zip(base, change)] + + +def _get_mac_offset(mac_addr: str, offset: int) -> str: + octets = [int(octet, 16) for octet in mac_addr.split(":")] + octets[5] = (octets[5] + offset) % 256 + return ":".join(f"{octet:02x}" for octet in octets) + + +def _off_by_one_mac(firmware: str) -> bool: + """Check if the firmware version has the off by one mac.""" + return bool(firmware and AwesomeVersion(firmware) >= FIX_MAC_FW) + + +def get_real_mac_addr(mac_addr: str, firmware: str) -> str: + """Increment the last byte of the mac address by one for FW>3.70.""" + return _get_mac_offset(mac_addr, 1) if _off_by_one_mac(firmware) else mac_addr + + +def formatted_serial(serial_number: str) -> str: + """Format the serial number to match the HA device registry.""" + return dr.format_mac(serial_number) + + +def mac_matches_serial_number(mac_addr: str, serial_number: str) -> bool: + """Check if a mac address matches the serial number.""" + formatted_mac = dr.format_mac(mac_addr) + return bool( + formatted_serial(serial_number) == formatted_mac + or _get_mac_offset(serial_number, 1) == formatted_mac + ) + + +async def async_execute_lifx(method: Callable) -> Message: + """Execute a lifx coroutine and wait for a response.""" + future: asyncio.Future[Message] = asyncio.Future() + + def _callback(bulb: Light, message: Message) -> None: + if not future.done(): + # The future will get canceled out from under + # us by async_timeout when we hit the OVERALL_TIMEOUT + future.set_result(message) + + _LOGGER.debug("Sending LIFX command: %s", method) + + method(callb=_callback) + result = None + + async with async_timeout.timeout(OVERALL_TIMEOUT): + result = await future + + if result is None: + raise asyncio.TimeoutError("No response from LIFX bulb") + return result diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index c062870f3e3..fb8000f8393 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -58,6 +58,8 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'isy994', 'registered_devices': True}, {'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'}, {'domain': 'isy994', 'hostname': 'polisy*', 'macaddress': '000DB9*'}, + {'domain': 'lifx', 'macaddress': 'D073D5*'}, + {'domain': 'lifx', 'registered_devices': True}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': 'B82CA0*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '00D02D*'}, diff --git a/mypy.ini b/mypy.ini index 5a3cbd7a05f..2333c20c4d8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1369,6 +1369,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lifx.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.local_ip.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 92e8fa4f9e9..230e6b3a7a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,6 +186,9 @@ aiokafka==0.7.2 # homeassistant.components.kef aiokef==0.2.16 +# homeassistant.components.lifx +aiolifx-connection==1.0.0 + # homeassistant.components.lifx aiolifx==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5153abbf447..9578e14fc6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,6 +164,15 @@ aiohue==4.4.2 # homeassistant.components.apache_kafka aiokafka==0.7.2 +# homeassistant.components.lifx +aiolifx-connection==1.0.0 + +# homeassistant.components.lifx +aiolifx==0.8.1 + +# homeassistant.components.lifx +aiolifx_effects==0.2.2 + # homeassistant.components.lookin aiolookin==0.1.1 diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py new file mode 100644 index 00000000000..fdea992c87d --- /dev/null +++ b/tests/components/lifx/__init__.py @@ -0,0 +1,217 @@ +"""Tests for the lifx integration.""" +from __future__ import annotations + +import asyncio +from contextlib import contextmanager +from unittest.mock import AsyncMock, MagicMock, patch + +from aiolifx.aiolifx import Light + +from homeassistant.components.lifx import discovery +from homeassistant.components.lifx.const import TARGET_ANY + +MODULE = "homeassistant.components.lifx" +MODULE_CONFIG_FLOW = "homeassistant.components.lifx.config_flow" +IP_ADDRESS = "127.0.0.1" +LABEL = "My Bulb" +SERIAL = "aa:bb:cc:dd:ee:cc" +MAC_ADDRESS = "aa:bb:cc:dd:ee:cd" +DEFAULT_ENTRY_TITLE = LABEL + + +class MockMessage: + """Mock a lifx message.""" + + def __init__(self): + """Init message.""" + self.target_addr = SERIAL + self.count = 9 + + +class MockFailingLifxCommand: + """Mock a lifx command that fails.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.calls = [] + + def __call__(self, *args, **kwargs): + """Call command.""" + if callb := kwargs.get("callb"): + callb(self.bulb, None) + self.calls.append([args, kwargs]) + + def reset_mock(self): + """Reset mock.""" + self.calls = [] + + +class MockLifxCommand: + """Mock a lifx command.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.calls = [] + + def __call__(self, *args, **kwargs): + """Call command.""" + if callb := kwargs.get("callb"): + callb(self.bulb, MockMessage()) + self.calls.append([args, kwargs]) + + def reset_mock(self): + """Reset mock.""" + self.calls = [] + + +def _mocked_bulb() -> Light: + bulb = Light(asyncio.get_running_loop(), SERIAL, IP_ADDRESS) + bulb.host_firmware_version = "3.00" + bulb.label = LABEL + bulb.color = [1, 2, 3, 4] + bulb.power_level = 0 + bulb.try_sending = AsyncMock() + bulb.set_infrared = MockLifxCommand(bulb) + bulb.get_color = MockLifxCommand(bulb) + bulb.set_power = MockLifxCommand(bulb) + bulb.set_color = MockLifxCommand(bulb) + bulb.get_hostfirmware = MockLifxCommand(bulb) + bulb.get_version = MockLifxCommand(bulb) + bulb.product = 1 # LIFX Original 1000 + return bulb + + +def _mocked_failing_bulb() -> Light: + bulb = _mocked_bulb() + bulb.get_color = MockFailingLifxCommand(bulb) + bulb.set_power = MockFailingLifxCommand(bulb) + bulb.set_color = MockFailingLifxCommand(bulb) + bulb.get_hostfirmware = MockFailingLifxCommand(bulb) + bulb.get_version = MockFailingLifxCommand(bulb) + return bulb + + +def _mocked_white_bulb() -> Light: + bulb = _mocked_bulb() + bulb.product = 19 # LIFX White 900 BR30 (High Voltage) + return bulb + + +def _mocked_brightness_bulb() -> Light: + bulb = _mocked_bulb() + bulb.product = 51 # LIFX Mini White + return bulb + + +def _mocked_light_strip() -> Light: + bulb = _mocked_bulb() + bulb.product = 31 # LIFX Z + bulb.get_color_zones = MockLifxCommand(bulb) + bulb.set_color_zones = MockLifxCommand(bulb) + bulb.color_zones = [MagicMock(), MagicMock()] + return bulb + + +def _mocked_bulb_new_firmware() -> Light: + bulb = _mocked_bulb() + bulb.host_firmware_version = "3.90" + return bulb + + +def _mocked_relay() -> Light: + bulb = _mocked_bulb() + bulb.product = 70 # LIFX Switch + return bulb + + +def _patch_device(device: Light | None = None, no_device: bool = False): + """Patch out discovery.""" + + class MockLifxConnecton: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + if no_device: + self.device = _mocked_failing_bulb() + else: + self.device = device or _mocked_bulb() + self.device.mac_addr = TARGET_ANY + + async def async_setup(self): + """Mock setup.""" + + def async_stop(self): + """Mock teardown.""" + + @contextmanager + def _patcher(): + with patch("homeassistant.components.lifx.LIFXConnection", MockLifxConnecton): + yield + + return _patcher() + + +def _patch_discovery(device: Light | None = None, no_device: bool = False): + """Patch out discovery.""" + + class MockLifxDiscovery: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init discovery.""" + if no_device: + self.lights = {} + return + discovered = device or _mocked_bulb() + self.lights = {discovered.mac_addr: discovered} + + def start(self): + """Mock start.""" + + def cleanup(self): + """Mock cleanup.""" + + @contextmanager + def _patcher(): + with patch.object(discovery, "DEFAULT_TIMEOUT", 0), patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + ): + yield + + return _patcher() + + +def _patch_config_flow_try_connect( + device: Light | None = None, no_device: bool = False +): + """Patch out discovery.""" + + class MockLifxConnecton: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + if no_device: + self.device = _mocked_failing_bulb() + else: + self.device = device or _mocked_bulb() + self.device.mac_addr = TARGET_ANY + + async def async_setup(self): + """Mock setup.""" + + def async_stop(self): + """Mock teardown.""" + + @contextmanager + def _patcher(): + with patch( + "homeassistant.components.lifx.config_flow.LIFXConnection", + MockLifxConnecton, + ): + yield + + return _patcher() diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py new file mode 100644 index 00000000000..326c4f75413 --- /dev/null +++ b/tests/components/lifx/conftest.py @@ -0,0 +1,57 @@ +"""Tests for the lifx integration.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from tests.common import mock_device_registry, mock_registry + + +@pytest.fixture +def mock_effect_conductor(): + """Mock the effect conductor.""" + + class MockConductor: + def __init__(self, *args, **kwargs) -> None: + """Mock the conductor.""" + self.start = AsyncMock() + self.stop = AsyncMock() + + def effect(self, bulb): + """Mock effect.""" + return MagicMock() + + mock_conductor = MockConductor() + + with patch( + "homeassistant.components.lifx.manager.aiolifx_effects.Conductor", + return_value=mock_conductor, + ): + yield mock_conductor + + +@pytest.fixture(autouse=True) +def lifx_mock_get_source_ip(mock_get_source_ip): + """Mock network util's async_get_source_ip.""" + + +@pytest.fixture(autouse=True) +def lifx_mock_async_get_ipv4_broadcast_addresses(): + """Mock network util's async_get_ipv4_broadcast_addresses.""" + with patch( + "homeassistant.components.network.async_get_ipv4_broadcast_addresses", + return_value=["255.255.255.255"], + ): + yield + + +@pytest.fixture(name="device_reg") +def device_reg_fixture(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture(name="entity_reg") +def entity_reg_fixture(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py new file mode 100644 index 00000000000..f007e9ee0e8 --- /dev/null +++ b/tests/components/lifx/test_config_flow.py @@ -0,0 +1,508 @@ +"""Tests for the lifx integration config flow.""" +import socket +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components import dhcp, zeroconf +from homeassistant.components.lifx import DOMAIN +from homeassistant.components.lifx.const import CONF_SERIAL +from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + LABEL, + MAC_ADDRESS, + MODULE, + SERIAL, + _mocked_failing_bulb, + _mocked_relay, + _patch_config_flow_try_connect, + _patch_discovery, +) + +from tests.common import MockConfigEntry + + +async def test_discovery(hass: HomeAssistant): + """Test setting up discovery.""" + with _patch_discovery(), _patch_config_flow_try_connect(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + # test we can try again + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_setup, patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DEVICE: SERIAL}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == DEFAULT_ENTRY_TITLE + assert result3["data"] == {CONF_HOST: IP_ADDRESS} + mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() + + # ignore configured devices + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_discovery_but_cannot_connect(hass: HomeAssistant): + """Test we can discover the device but we cannot connect.""" + with _patch_discovery(), _patch_config_flow_try_connect(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DEVICE: SERIAL}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "cannot_connect" + + +async def test_discovery_with_existing_device_present(hass: HomeAssistant): + """Test setting up discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id="dd:dd:dd:dd:dd:dd" + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_config_flow_try_connect(no_device=True): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + # Now abort and make sure we can start over + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_DEVICE: SERIAL} + ) + assert result3["type"] == "create_entry" + assert result3["title"] == DEFAULT_ENTRY_TITLE + assert result3["data"] == { + CONF_HOST: IP_ADDRESS, + } + await hass.async_block_till_done() + + mock_setup_entry.assert_called_once() + + # ignore configured devices + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_discovery_no_device(hass: HomeAssistant): + """Test discovery without device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_manual(hass: HomeAssistant): + """Test manually setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + # Cannot connect (timeout) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + # Success + with _patch_discovery(), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup", return_value=True + ), patch(f"{MODULE}.async_setup_entry", return_value=True): + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + assert result4["type"] == "create_entry" + assert result4["title"] == DEFAULT_ENTRY_TITLE + assert result4["data"] == { + CONF_HOST: IP_ADDRESS, + } + + # Duplicate + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_manual_dns_error(hass: HomeAssistant): + """Test manually setup with unresolving host.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + class MockLifxConnectonDnsError: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + self.device = _mocked_failing_bulb() + + async def async_setup(self): + """Mock setup.""" + raise socket.gaierror() + + def async_stop(self): + """Mock teardown.""" + + # Cannot connect due to dns error + with _patch_discovery(no_device=True), patch( + "homeassistant.components.lifx.config_flow.LIFXConnection", + MockLifxConnectonDnsError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: "does.not.resolve"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_manual_no_capabilities(hass: HomeAssistant): + """Test manually setup without successful get_capabilities.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup", return_value=True + ), patch(f"{MODULE}.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["data"] == { + CONF_HOST: IP_ADDRESS, + } + + +async def test_discovered_by_discovery_and_dhcp(hass): + """Test we get the form with discovery and abort for dhcp source when we get both.""" + + with _patch_discovery(), _patch_config_flow_try_connect(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL + ), + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + with _patch_discovery(), _patch_config_flow_try_connect(): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress="00:00:00:00:00:00", hostname="mock_hostname" + ), + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_in_progress" + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.5", macaddress="00:00:00:00:00:01", hostname="mock_hostname" + ), + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "cannot_connect" + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname=LABEL, + name=LABEL, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + type="mock_type", + ), + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + {CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, + ), + ], +) +async def test_discovered_by_dhcp_or_discovery(hass, source, data): + """Test we can setup when discovered from dhcp or discovery.""" + + with _patch_discovery(), _patch_config_flow_try_connect(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_async_setup, patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_async_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["data"] == { + CONF_HOST: IP_ADDRESS, + } + assert mock_async_setup.called + assert mock_async_setup_entry.called + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname=LABEL, + name=LABEL, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + type="mock_type", + ), + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + {CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, + ), + ], +) +async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source, data): + """Test we abort if we cannot get the unique id when discovered from dhcp.""" + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_discovered_by_dhcp_updates_ip(hass): + """Update host from dhcp.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL + ), + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == IP_ADDRESS + + +async def test_refuse_relays(hass: HomeAssistant): + """Test we refuse to setup relays.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(device=_mocked_relay()), _patch_config_flow_try_connect( + device=_mocked_relay() + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/lifx/test_init.py b/tests/components/lifx/test_init.py new file mode 100644 index 00000000000..5424ae3c3fc --- /dev/null +++ b/tests/components/lifx/test_init.py @@ -0,0 +1,150 @@ +"""Tests for the lifx component.""" +from __future__ import annotations + +from datetime import timedelta +import socket +from unittest.mock import patch + +from homeassistant.components import lifx +from homeassistant.components.lifx import DOMAIN, discovery +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + IP_ADDRESS, + SERIAL, + MockFailingLifxCommand, + _mocked_bulb, + _mocked_failing_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_configuring_lifx_causes_discovery(hass): + """Test that specifying empty config does discovery.""" + start_calls = 0 + + class MockLifxDiscovery: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init discovery.""" + discovered = _mocked_bulb() + self.lights = {discovered.mac_addr: discovered} + + def start(self): + """Mock start.""" + nonlocal start_calls + start_calls += 1 + + def cleanup(self): + """Mock cleanup.""" + + with _patch_config_flow_try_connect(), patch.object( + discovery, "DEFAULT_TIMEOUT", 0 + ), patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert start_calls == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert start_calls == 1 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + assert start_calls == 2 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) + await hass.async_block_till_done() + assert start_calls == 3 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30)) + await hass.async_block_till_done() + assert start_calls == 4 + + +async def test_config_entry_reload(hass): + """Test that a config entry can be reloaded.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.LOADED + await hass.config_entries.async_unload(already_migrated_config_entry.entry_id) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_config_entry_retry(hass): + """Test that a config entry can be retried.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ), _patch_device(no_device=True): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_get_version_fails(hass): + """Test we handle get version failing.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.product = None + bulb.host_firmware_version = None + bulb.get_version = MockFailingLifxCommand(bulb) + + with _patch_discovery(device=bulb), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_dns_error_at_startup(hass): + """Test we handle get version failing.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_failing_bulb() + + class MockLifxConnectonDnsError: + """Mock lifx connection with a dns error.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + self.device = bulb + + async def async_setup(self): + """Mock setup.""" + raise socket.gaierror() + + def async_stop(self): + """Mock teardown.""" + + # Cannot connect due to dns error + with _patch_discovery(device=bulb), patch( + "homeassistant.components.lifx.LIFXConnection", + MockLifxConnectonDnsError, + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py new file mode 100644 index 00000000000..5b641e850f2 --- /dev/null +++ b/tests/components/lifx/test_light.py @@ -0,0 +1,993 @@ +"""Tests for the lifx integration light platform.""" + +from datetime import timedelta +from unittest.mock import patch + +import aiolifx_effects +import pytest + +from homeassistant.components import lifx +from homeassistant.components.lifx import DOMAIN +from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES +from homeassistant.components.lifx.manager import SERVICE_EFFECT_COLORLOOP +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + ATTR_TRANSITION, + ATTR_XY_COLOR, + DOMAIN as LIGHT_DOMAIN, + ColorMode, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + IP_ADDRESS, + MAC_ADDRESS, + SERIAL, + MockFailingLifxCommand, + MockLifxCommand, + MockMessage, + _mocked_brightness_bulb, + _mocked_bulb, + _mocked_bulb_new_firmware, + _mocked_light_strip, + _mocked_white_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_light_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "1.2.3.4"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, SERIAL)} + ) + assert device.identifiers == {(DOMAIN, SERIAL)} + + +async def test_light_unique_id_new_firmware(hass: HomeAssistant) -> None: + """Test a light unique id with newer firmware.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "1.2.3.4"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers=set(), + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + ) + assert device.identifiers == {(DOMAIN, SERIAL)} + + +@patch("homeassistant.components.lifx.light.COLOR_ZONE_POPULATE_DELAY", 0) +async def test_light_strip(hass: HomeAssistant) -> None: + """Test a light strip.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_light_strip() + bulb.power_level = 65535 + bulb.color = [65535, 65535, 65535, 65535] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == ColorMode.HS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (360.0, 100.0) + assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) + assert attributes[ATTR_XY_COLOR] == (0.701, 0.299) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + bulb.set_color_zones.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + bulb.set_color_zones.reset_mock() + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + # Single color uses the fast path + assert bulb.set_color.calls[0][0][0] == [1820, 19660, 65535, 3500] + bulb.set_color.reset_mock() + assert len(bulb.set_color_zones.calls) == 0 + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 10, 30)}, + blocking=True, + ) + # Single color uses the fast path + assert bulb.set_color.calls[0][0][0] == [64643, 62964, 65535, 3500] + bulb.set_color.reset_mock() + assert len(bulb.set_color_zones.calls) == 0 + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.3, 0.7)}, + blocking=True, + ) + # Single color uses the fast path + assert bulb.set_color.calls[0][0][0] == [15848, 65535, 65535, 3500] + bulb.set_color.reset_mock() + assert len(bulb.set_color_zones.calls) == 0 + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + # multiple zones in effect and we are changing the brightness + # we need to do each zone individually + assert len(bulb.set_color.calls) == 0 + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [0, 65535, 32896, 3500], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + call_dict = bulb.set_color_zones.calls[1][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [54612, 65535, 32896, 3500], + "duration": 0, + "end_index": 1, + "start_index": 1, + } + call_dict = bulb.set_color_zones.calls[7][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 1, + "color": [46420, 65535, 32896, 3500], + "duration": 0, + "end_index": 7, + "start_index": 7, + } + bulb.set_color_zones.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [0, 2], + }, + blocking=True, + ) + # set a two zones + assert len(bulb.set_color.calls) == 0 + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [0, 0, 65535, 3500], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + call_dict = bulb.set_color_zones.calls[1][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 1, + "color": [0, 0, 65535, 3500], + "duration": 0, + "end_index": 2, + "start_index": 2, + } + bulb.set_color_zones.reset_mock() + + bulb.get_color_zones.reset_mock() + bulb.set_power.reset_mock() + + bulb.power_level = 0 + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 255, 255), ATTR_ZONES: [3]}, + blocking=True, + ) + # set a one zone + assert len(bulb.set_power.calls) == 2 + assert len(bulb.get_color_zones.calls) == 2 + assert len(bulb.set_color.calls) == 0 + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 1, + "color": [0, 0, 65535, 3500], + "duration": 0, + "end_index": 3, + "start_index": 3, + } + bulb.get_color_zones.reset_mock() + bulb.set_power.reset_mock() + bulb.set_color_zones.reset_mock() + + bulb.set_color_zones = MockFailingLifxCommand(bulb) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + bulb.set_color_zones = MockLifxCommand(bulb) + bulb.get_color_zones = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + bulb.get_color_zones = MockLifxCommand(bulb) + bulb.get_color = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + +async def test_color_light_with_temp( + hass: HomeAssistant, mock_effect_conductor +) -> None: + """Test a color light with temp.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.power_level = 65535 + bulb.color = [65535, 65535, 65535, 65535] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == ColorMode.HS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (360.0, 100.0) + assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) + assert attributes[ATTR_XY_COLOR] == (0.701, 0.299) + + bulb.color = [32000, None, 32000, 6000] + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (31.007, 6.862) + assert attributes[ATTR_RGB_COLOR] == (255, 246, 237) + assert attributes[ATTR_XY_COLOR] == (0.339, 0.338) + bulb.color = [65535, 65535, 65535, 65535] + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [65535, 65535, 25700, 65535] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [1820, 19660, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 30, 80)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [63107, 57824, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.46, 0.376)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [4956, 30583, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_colorloop"}, + blocking=True, + ) + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectColorloop) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT_COLORLOOP, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectColorloop) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_pulse"}, + blocking=True, + ) + assert len(mock_effect_conductor.stop.mock_calls) == 1 + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectPulse) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_stop"}, + blocking=True, + ) + assert len(mock_effect_conductor.stop.mock_calls) == 2 + + +async def test_white_bulb(hass: HomeAssistant) -> None: + """Test a white bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_white_bulb() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ] + assert attributes[ATTR_COLOR_TEMP] == 166 + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 25700, 6000] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 32000, 2500] + bulb.set_color.reset_mock() + + +async def test_config_zoned_light_strip_fails(hass): + """Test we handle failure to update zones.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + light_strip = _mocked_light_strip() + entity_id = "light.my_bulb" + + class MockFailingLifxCommand: + """Mock a lifx command that fails on the 3rd try.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.call_count = 0 + + def __call__(self, callb=None, *args, **kwargs): + """Call command.""" + self.call_count += 1 + response = None if self.call_count >= 3 else MockMessage() + if callb: + callb(self.bulb, response) + + light_strip.get_color_zones = MockFailingLifxCommand(light_strip) + + with _patch_discovery(device=light_strip), _patch_device(device=light_strip): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + assert hass.states.get(entity_id).state == STATE_OFF + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_white_light_fails(hass): + """Test we handle failure to power on off.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_white_bulb() + entity_id = "light.my_bulb" + + bulb.set_power = MockFailingLifxCommand(bulb) + + with _patch_discovery(device=bulb), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + assert hass.states.get(entity_id).state == STATE_OFF + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + bulb.set_power = MockLifxCommand(bulb) + bulb.set_color = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [1, 0, 3, 6535] + bulb.set_color.reset_mock() + + +async def test_brightness_bulb(hass: HomeAssistant) -> None: + """Test a brightness only bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_brightness_bulb() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.BRIGHTNESS, + ] + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 25700, 6000] + bulb.set_color.reset_mock() + + +async def test_transitions_brightness_only(hass: HomeAssistant) -> None: + """Test transitions with a brightness only device.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_brightness_bulb() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.BRIGHTNESS, + ] + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + bulb.power_level = 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 5, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is True + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + + bulb.power_level = 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 5, ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is True + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + + await hass.async_block_till_done() + bulb.get_color.reset_mock() + + # Ensure we force an update after the transition + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(bulb.get_color.calls) == 2 + + +async def test_transitions_color_bulb(hass: HomeAssistant) -> None: + """Test transitions with a color bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + bulb.power_level = 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is False + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 0} # already off + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_RGB_COLOR: (255, 5, 10), + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [65316, 64249, 25700, 3500] + assert bulb.set_power.calls[0][0][0] is True + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + bulb.power_level = 12800 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_RGB_COLOR: (5, 5, 10), + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + ATTR_BRIGHTNESS: 200, + }, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [43690, 32767, 51400, 3500] + call_dict = bulb.set_color.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + await hass.async_block_till_done() + bulb.get_color.reset_mock() + + # Ensure we force an update after the transition + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(bulb.get_color.calls) == 2 + + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is False + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + +async def test_infrared_color_bulb(hass: HomeAssistant) -> None: + """Test setting infrared with a color bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_INFRARED: 100, + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert bulb.set_infrared.calls[0][0][0] == 25700 + + +async def test_color_bulb_is_actually_off(hass: HomeAssistant) -> None: + """Test setting a color when we think a bulb is on but its actually off.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + + class MockLifxCommandActuallyOff: + """Mock a lifx command that will update our power level state.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.calls = [] + + def __call__(self, *args, **kwargs): + """Call command.""" + bulb.power_level = 0 + if callb := kwargs.get("callb"): + callb(self.bulb, MockMessage()) + self.calls.append([args, kwargs]) + + bulb.set_color = MockLifxCommandActuallyOff(bulb) + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_RGB_COLOR: (100, 100, 100), + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [0, 0, 25700, 3500] + assert len(bulb.set_power.calls) == 1 diff --git a/tests/components/lifx/test_migration.py b/tests/components/lifx/test_migration.py new file mode 100644 index 00000000000..0f00034590b --- /dev/null +++ b/tests/components/lifx/test_migration.py @@ -0,0 +1,281 @@ +"""Tests the lifx migration.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from homeassistant import setup +from homeassistant.components import lifx +from homeassistant.components.lifx import DOMAIN, discovery +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + IP_ADDRESS, + LABEL, + MAC_ADDRESS, + SERIAL, + _mocked_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_migration_device_online_end_to_end( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test migration from single config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + light_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=dr.format_mac(SERIAL), + original_name=LABEL, + device_id=device.id, + ) + + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + migrated_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + migrated_entry = entry + break + + assert migrated_entry is not None + + assert device.config_entries == {migrated_entry.entry_id} + assert light_entity_reg.config_entry_id == migrated_entry.entry_id + assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + legacy_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + legacy_entry = entry + break + + assert legacy_entry is None + + +async def test_discovery_is_more_frequent_during_migration( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test that discovery is more frequent during migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=dr.format_mac(SERIAL), + original_name=LABEL, + device_id=device.id, + ) + + bulb = _mocked_bulb() + start_calls = 0 + + class MockLifxDiscovery: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init discovery.""" + self.bulb = bulb + self.lights = {} + + def start(self): + """Mock start.""" + nonlocal start_calls + start_calls += 1 + # Discover the bulb so we can complete migration + # and verify we switch back to normal discovery + # interval + if start_calls == 4: + self.lights = {self.bulb.mac_addr: self.bulb} + + def cleanup(self): + """Mock cleanup.""" + + with _patch_device(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), patch.object(discovery, "DEFAULT_TIMEOUT", 0), patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert start_calls == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert start_calls == 1 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + assert start_calls == 3 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + assert start_calls == 4 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) + await hass.async_block_till_done() + assert start_calls == 5 + + +async def test_migration_device_online_end_to_end_after_downgrade( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test migration from single config entry can happen again after a downgrade.""" + config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + config_entry.add_to_hass(hass) + + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + light_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=SERIAL, + original_name=LABEL, + device_id=device.id, + ) + + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + assert device.config_entries == {config_entry.entry_id} + assert light_entity_reg.config_entry_id == config_entry.entry_id + assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] + + legacy_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + legacy_entry = entry + break + + assert legacy_entry is None + + +async def test_migration_device_online_end_to_end_ignores_other_devices( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test migration from single config entry.""" + legacy_config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + legacy_config_entry.add_to_hass(hass) + + other_domain_config_entry = MockConfigEntry( + domain="other_domain", data={}, unique_id="other_domain" + ) + other_domain_config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=legacy_config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + other_device = device_reg.async_get_or_create( + config_entry_id=other_domain_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "556655665566")}, + name=LABEL, + ) + light_entity_reg = entity_reg.async_get_or_create( + config_entry=legacy_config_entry, + platform=DOMAIN, + domain="light", + unique_id=SERIAL, + original_name=LABEL, + device_id=device.id, + ) + ignored_entity_reg = entity_reg.async_get_or_create( + config_entry=other_domain_config_entry, + platform=DOMAIN, + domain="sensor", + unique_id="00:00:00:00:00:00_sensor", + original_name=LABEL, + device_id=device.id, + ) + garbage_entity_reg = entity_reg.async_get_or_create( + config_entry=legacy_config_entry, + platform=DOMAIN, + domain="sensor", + unique_id="garbage", + original_name=LABEL, + device_id=other_device.id, + ) + + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + new_entry = None + legacy_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + legacy_entry = entry + else: + new_entry = entry + + assert new_entry is not None + assert legacy_entry is None + + assert device.config_entries == {legacy_config_entry.entry_id} + assert light_entity_reg.config_entry_id == legacy_config_entry.entry_id + assert ignored_entity_reg.config_entry_id == other_domain_config_entry.entry_id + assert garbage_entity_reg.config_entry_id == legacy_config_entry.entry_id + + assert er.async_entries_for_config_entry(entity_reg, legacy_config_entry) == [] + assert dr.async_entries_for_config_entry(device_reg, legacy_config_entry) == [] From 41e4b38c3a37d580aa103aae520aee2f32102484 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 17:58:08 -0500 Subject: [PATCH 540/820] Add device and advertisement to BluetoothServiceInfoBleak (#75381) --- .../components/bluetooth/__init__.py | 71 ++++++++++++++++--- homeassistant/components/bluetooth/models.py | 7 +- tests/components/bluetooth/test_init.py | 42 +++++++++++ 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index ea058a72759..8a1897c5ec2 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations from collections.abc import Callable +from dataclasses import dataclass from enum import Enum import fnmatch import logging import platform -from typing import Final, TypedDict +from typing import Final, TypedDict, Union from bleak import BleakError from bleak.backends.device import BLEDevice @@ -42,6 +43,37 @@ MAX_REMEMBER_ADDRESSES: Final = 2048 SOURCE_LOCAL: Final = "local" +@dataclass +class BluetoothServiceInfoBleak(BluetoothServiceInfo): # type: ignore[misc] + """BluetoothServiceInfo with bleak data. + + Integrations may need BLEDevice and AdvertisementData + to connect to the device without having bleak trigger + another scan to translate the address to the system's + internal details. + """ + + device: BLEDevice + advertisement: AdvertisementData + + @classmethod + def from_advertisement( + cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str + ) -> BluetoothServiceInfo: + """Create a BluetoothServiceInfoBleak from an advertisement.""" + return cls( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=source, + device=device, + advertisement=advertisement_data, + ) + + class BluetoothCallbackMatcherOptional(TypedDict, total=False): """Matcher for the bluetooth integration for callback optional fields.""" @@ -75,13 +107,15 @@ MANUFACTURER_DATA_START: Final = "manufacturer_data_start" BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") -BluetoothCallback = Callable[[BluetoothServiceInfo, BluetoothChange], None] +BluetoothCallback = Callable[ + [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None +] @hass_callback def async_discovered_service_info( hass: HomeAssistant, -) -> list[BluetoothServiceInfo]: +) -> list[BluetoothServiceInfoBleak]: """Return the discovered devices list.""" if DOMAIN not in hass.data: return [] @@ -89,6 +123,18 @@ def async_discovered_service_info( return manager.async_discovered_service_info() +@hass_callback +def async_ble_device_from_address( + hass: HomeAssistant, + address: str, +) -> BLEDevice | None: + """Return BLEDevice for an address if its present.""" + if DOMAIN not in hass.data: + return None + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_ble_device_from_address(address) + + @hass_callback def async_address_present( hass: HomeAssistant, @@ -263,13 +309,13 @@ class BluetoothManager: if not matched_domains and not self._callbacks: return - service_info: BluetoothServiceInfo | None = None + service_info: BluetoothServiceInfoBleak | None = None for callback, matcher in self._callbacks: if matcher is None or _ble_device_matches( matcher, device, advertisement_data ): if service_info is None: - service_info = BluetoothServiceInfo.from_advertisement( + service_info = BluetoothServiceInfoBleak.from_advertisement( device, advertisement_data, SOURCE_LOCAL ) try: @@ -280,7 +326,7 @@ class BluetoothManager: if not matched_domains: return if service_info is None: - service_info = BluetoothServiceInfo.from_advertisement( + service_info = BluetoothServiceInfoBleak.from_advertisement( device, advertisement_data, SOURCE_LOCAL ) for domain in matched_domains: @@ -316,7 +362,7 @@ class BluetoothManager: ): try: callback( - BluetoothServiceInfo.from_advertisement( + BluetoothServiceInfoBleak.from_advertisement( *device_adv_data, SOURCE_LOCAL ), BluetoothChange.ADVERTISEMENT, @@ -326,6 +372,15 @@ class BluetoothManager: return _async_remove_callback + @hass_callback + def async_ble_device_from_address(self, address: str) -> BLEDevice | None: + """Return the BLEDevice if present.""" + if models.HA_BLEAK_SCANNER and ( + ble_adv := models.HA_BLEAK_SCANNER.history.get(address) + ): + return ble_adv[0] + return None + @hass_callback def async_address_present(self, address: str) -> bool: """Return if the address is present.""" @@ -344,7 +399,7 @@ class BluetoothManager: discovered = models.HA_BLEAK_SCANNER.discovered_devices history = models.HA_BLEAK_SCANNER.history return [ - BluetoothServiceInfo.from_advertisement( + BluetoothServiceInfoBleak.from_advertisement( *history[device.address], SOURCE_LOCAL ) for device in discovered diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index eda94305aa9..fd7559aeefa 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import contextlib import logging from typing import Any, Final, cast @@ -56,7 +57,9 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] self._callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] - self.history: LRU = LRU(MAX_HISTORY_SIZE) + self.history: Mapping[str, tuple[BLEDevice, AdvertisementData]] = LRU( + MAX_HISTORY_SIZE + ) super().__init__(*args, **kwargs) @hass_callback @@ -87,7 +90,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] Here we get the actual callback from bleak and dispatch it to all the wrapped HaBleakScannerWrapper classes """ - self.history[device.address] = (device, advertisement_data) + self.history[device.address] = (device, advertisement_data) # type: ignore[index] for callback_filters in self._callbacks: _dispatch_callback(*callback_filters, device, advertisement_data) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a31f911f6a7..1a002e5e354 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -248,6 +248,8 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): # wrong_name should not appear because bleak no longer sees it assert service_infos[0].name == "wohand" assert service_infos[0].source == SOURCE_LOCAL + assert isinstance(service_infos[0].device, BLEDevice) + assert isinstance(service_infos[0].advertisement, AdvertisementData) assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True @@ -694,3 +696,43 @@ async def test_wrapped_instance_unsupported_filter( } ) assert "Only UUIDs filters are supported" in caplog.text + + +async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): + """Test the async_ble_device_from_address api.""" + mock_bt = [] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + assert not bluetooth.async_discovered_service_info(hass) + assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") + assert ( + bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") is None + ) + + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + assert not bluetooth.async_discovered_service_info(hass) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert ( + bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") + is switchbot_device + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, "00:66:33:22:11:22") is None + ) From b37f15b1d572c5215636cf3f90f609b25941541d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 19:16:24 -0500 Subject: [PATCH 541/820] Update bluetooth_le_tracker to use Bleak (#75013) Co-authored-by: Martin Hjelmare --- .../bluetooth_le_tracker/device_tracker.py | 185 ++++++------ .../bluetooth_le_tracker/manifest.json | 6 +- requirements_all.txt | 1 - requirements_test_all.txt | 4 - .../bluetooth_le_tracker/conftest.py | 7 + .../test_device_tracker.py | 273 +++++++++++++++++- 6 files changed, 370 insertions(+), 106 deletions(-) create mode 100644 tests/components/bluetooth_le_tracker/conftest.py diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index b44a180988d..f85cc2bad0a 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -2,19 +2,20 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging from uuid import UUID -import pygatt +from bleak import BleakClient, BleakError +from bleak.backends.device import BLEDevice import voluptuous as vol +from homeassistant.components import bluetooth from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, ) from homeassistant.components.device_tracker.const import ( - CONF_SCAN_INTERVAL, CONF_TRACK_NEW, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE, @@ -23,10 +24,10 @@ from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, async_load_config, ) -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -53,33 +54,31 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def setup_scanner( # noqa: C901 +async def async_setup_scanner( # noqa: C901 hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + async_see: Callable[..., Awaitable[None]], discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Bluetooth LE Scanner.""" new_devices: dict[str, dict] = {} - hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) - - def handle_stop(event): - """Try to shut down the bluetooth child process nicely.""" - # These should never be unset at the point this runs, but just for - # safety's sake, use `get`. - adapter = hass.data.get(DATA_BLE, {}).get(DATA_BLE_ADAPTER) - if adapter is not None: - adapter.kill() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) if config[CONF_TRACK_BATTERY]: battery_track_interval = config[CONF_TRACK_BATTERY_INTERVAL] else: battery_track_interval = timedelta(0) - def see_device(address, name, new_device=False, battery=None): + yaml_path = hass.config.path(YAML_DEVICES) + devs_to_track: set[str] = set() + devs_no_track: set[str] = set() + devs_track_battery = {} + interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + # if track new devices is true discover new devices + # on every scan. + track_new = config.get(CONF_TRACK_NEW) + + async def async_see_device(address, name, new_device=False, battery=None): """Mark a device as seen.""" if name is not None: name = name.strip("\x00") @@ -95,7 +94,7 @@ def setup_scanner( # noqa: C901 if new_devices[address]["seen"] < MIN_SEEN_NEW: return _LOGGER.debug("Adding %s to tracked devices", address) - devs_to_track.append(address) + devs_to_track.add(address) if battery_track_interval > timedelta(0): devs_track_battery[address] = dt_util.as_utc( datetime.fromtimestamp(0) @@ -105,109 +104,113 @@ def setup_scanner( # noqa: C901 new_devices[address] = {"seen": 1, "name": name} return - see( + await async_see( mac=BLE_PREFIX + address, host_name=name, source_type=SOURCE_TYPE_BLUETOOTH_LE, battery=battery, ) - def discover_ble_devices(): - """Discover Bluetooth LE devices.""" - _LOGGER.debug("Discovering Bluetooth LE devices") - try: - adapter = pygatt.GATTToolBackend() - hass.data[DATA_BLE][DATA_BLE_ADAPTER] = adapter - devs = adapter.scan() - - devices = {x["address"]: x["name"] for x in devs} - _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) - except (RuntimeError, pygatt.exceptions.BLEError) as error: - _LOGGER.error("Error during Bluetooth LE scan: %s", error) - return {} - return devices - - yaml_path = hass.config.path(YAML_DEVICES) - devs_to_track = [] - devs_donot_track = [] - devs_track_battery = {} - # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in asyncio.run_coroutine_threadsafe( - async_load_config(yaml_path, hass, timedelta(0)), hass.loop - ).result(): + for device in await async_load_config(yaml_path, hass, timedelta(0)): # check if device is a valid bluetooth device if device.mac and device.mac[:4].upper() == BLE_PREFIX: address = device.mac[4:] if device.track: _LOGGER.debug("Adding %s to BLE tracker", device.mac) - devs_to_track.append(address) + devs_to_track.add(address) if battery_track_interval > timedelta(0): devs_track_battery[address] = dt_util.as_utc( datetime.fromtimestamp(0) ) else: _LOGGER.debug("Adding %s to BLE do not track", device.mac) - devs_donot_track.append(address) - - # if track new devices is true discover new devices - # on every scan. - track_new = config.get(CONF_TRACK_NEW) + devs_no_track.add(address) if not devs_to_track and not track_new: _LOGGER.warning("No Bluetooth LE devices to track!") return False - interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - - def update_ble(now): + async def _async_see_update_ble_battery( + mac: str, + now: datetime, + service_info: bluetooth.BluetoothServiceInfo, + ) -> None: """Lookup Bluetooth LE devices and update status.""" - devs = discover_ble_devices() - if devs_track_battery: - adapter = hass.data[DATA_BLE][DATA_BLE_ADAPTER] - for mac in devs_to_track: - if mac not in devs: - continue + battery = None + ble_device: BLEDevice | str = ( + bluetooth.async_ble_device_from_address(hass, mac) or mac + ) + try: + async with BleakClient(ble_device) as client: + bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) + battery = ord(bat_char) + except asyncio.TimeoutError: + _LOGGER.warning( + "Timeout when trying to get battery status for %s", service_info.name + ) + # Bleak currently has a few places where checking dbus attributes + # can raise when there is another error. We need to trap AttributeError + # until bleak releases v0.15+ which resolves these. + except (AttributeError, BleakError) as err: + _LOGGER.debug("Could not read battery status: %s", err) + # If the device does not offer battery information, there is no point in asking again later on. + # Remove the device from the battery-tracked devices, so that their battery is not wasted + # trying to get an unavailable information. + del devs_track_battery[mac] + if battery: + await async_see_device(mac, service_info.name, battery=battery) - if devs[mac] is None: - devs[mac] = mac - - battery = None + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfo, change: bluetooth.BluetoothChange + ) -> None: + """Update from a ble callback.""" + mac = service_info.address + if mac in devs_to_track: + now = dt_util.utcnow() + hass.async_create_task(async_see_device(mac, service_info.name)) if ( mac in devs_track_battery and now > devs_track_battery[mac] + battery_track_interval ): - handle = None - try: - adapter.start(reset_on_start=False) - _LOGGER.debug("Reading battery for Bluetooth LE device %s", mac) - bt_device = adapter.connect(mac) - # Try to get the handle; it will raise a BLEError exception if not available - handle = bt_device.get_handle(BATTERY_CHARACTERISTIC_UUID) - battery = ord(bt_device.char_read(BATTERY_CHARACTERISTIC_UUID)) - devs_track_battery[mac] = now - except pygatt.exceptions.NotificationTimeout: - _LOGGER.warning("Timeout when trying to get battery status") - except pygatt.exceptions.BLEError as err: - _LOGGER.warning("Could not read battery status: %s", err) - if handle is not None: - # If the device does not offer battery information, there is no point in asking again later on. - # Remove the device from the battery-tracked devices, so that their battery is not wasted - # trying to get an unavailable information. - del devs_track_battery[mac] - finally: - adapter.stop() - see_device(mac, devs[mac], battery=battery) + devs_track_battery[mac] = now + asyncio.create_task( + _async_see_update_ble_battery(mac, now, service_info) + ) if track_new: - for address in devs: - if address not in devs_to_track and address not in devs_donot_track: - _LOGGER.info("Discovered Bluetooth LE device %s", address) - see_device(address, devs[address], new_device=True) + if mac not in devs_to_track and mac not in devs_no_track: + _LOGGER.info("Discovered Bluetooth LE device %s", mac) + hass.async_create_task( + async_see_device(mac, service_info.name, new_device=True) + ) - track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval) + @callback + def _async_refresh_ble(now: datetime) -> None: + """Refresh BLE devices from the discovered service info.""" + # Make sure devices are seen again at the scheduled + # interval so they do not get set to not_home when + # there have been no callbacks because the RSSI or + # other properties have not changed. + for service_info in bluetooth.async_discovered_service_info(hass): + _async_update_ble(service_info, bluetooth.BluetoothChange.ADVERTISEMENT) + + cancels = [ + bluetooth.async_register_callback(hass, _async_update_ble, None), + async_track_time_interval(hass, _async_refresh_ble, interval), + ] + + @callback + def _async_handle_stop(event: Event) -> None: + """Cancel the callback.""" + for cancel in cancels: + cancel() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_handle_stop) + + _async_refresh_ble(dt_util.now()) - update_ble(dt_util.utcnow()) return True diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 7552c024d62..6d1d4ba2d4a 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -2,8 +2,8 @@ "domain": "bluetooth_le_tracker", "name": "Bluetooth LE Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", - "requirements": ["pygatt[GATTTOOL]==4.0.5"], + "dependencies": ["bluetooth"], "codeowners": [], - "iot_class": "local_polling", - "loggers": ["pygatt"] + "iot_class": "local_push", + "loggers": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 230e6b3a7a6..d43ee87da12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1530,7 +1530,6 @@ pyfronius==0.7.1 # homeassistant.components.ifttt pyfttt==0.3 -# homeassistant.components.bluetooth_le_tracker # homeassistant.components.skybeacon pygatt[GATTTOOL]==4.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9578e14fc6d..ee02d6f05a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1042,10 +1042,6 @@ pyfronius==0.7.1 # homeassistant.components.ifttt pyfttt==0.3 -# homeassistant.components.bluetooth_le_tracker -# homeassistant.components.skybeacon -pygatt[GATTTOOL]==4.0.5 - # homeassistant.components.hvv_departures pygti==0.9.2 diff --git a/tests/components/bluetooth_le_tracker/conftest.py b/tests/components/bluetooth_le_tracker/conftest.py new file mode 100644 index 00000000000..30b2d5a44fb --- /dev/null +++ b/tests/components/bluetooth_le_tracker/conftest.py @@ -0,0 +1,7 @@ +"""Tests for the bluetooth_le_tracker component.""" +import pytest + + +@pytest.fixture(autouse=True) +def bluetooth_le_tracker_auto_mock_bluetooth(mock_bluetooth): + """Mock the bluetooth integration scanner.""" diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 308371c9aaa..dfe47b38b33 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,9 +1,17 @@ """Test Bluetooth LE device tracker.""" +import asyncio from datetime import timedelta from unittest.mock import patch +from bleak import BleakError + +from homeassistant.components.bluetooth import BluetoothServiceInfo from homeassistant.components.bluetooth_le_tracker import device_tracker +from homeassistant.components.bluetooth_le_tracker.device_tracker import ( + CONF_TRACK_BATTERY, + CONF_TRACK_BATTERY_INTERVAL, +) from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, @@ -16,7 +24,49 @@ from homeassistant.util import dt as dt_util, slugify from tests.common import async_fire_time_changed -async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): +class MockBleakClient: + """Mock BleakClient.""" + + def __init__(self, *args, **kwargs): + """Mock BleakClient.""" + pass + + async def __aenter__(self, *args, **kwargs): + """Mock BleakClient.__aenter__.""" + return self + + async def __aexit__(self, *args, **kwargs): + """Mock BleakClient.__aexit__.""" + pass + + +class MockBleakClientTimesOut(MockBleakClient): + """Mock BleakClient that times out.""" + + async def read_gatt_char(self, *args, **kwargs): + """Mock BleakClient.read_gatt_char.""" + raise asyncio.TimeoutError + + +class MockBleakClientFailing(MockBleakClient): + """Mock BleakClient that fails.""" + + async def read_gatt_char(self, *args, **kwargs): + """Mock BleakClient.read_gatt_char.""" + raise BleakError("Failed") + + +class MockBleakClientBattery5(MockBleakClient): + """Mock BleakClient that returns a battery level of 5.""" + + async def read_gatt_char(self, *args, **kwargs): + """Mock BleakClient.read_gatt_char.""" + return b"\x05" + + +async def test_preserve_new_tracked_device_name( + hass, mock_bluetooth, mock_device_tracker_conf +): """Test preserving tracked device name across new seens.""" address = "DE:AD:BE:EF:13:37" @@ -24,13 +74,22 @@ async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): entity_id = f"{DOMAIN}.{slugify(name)}" with patch( - "homeassistant.components." - "bluetooth_le_tracker.device_tracker.pygatt.GATTToolBackend" - ) as mock_backend, patch.object(device_tracker, "MIN_SEEN_NEW", 3): + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + device = BluetoothServiceInfo( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) # Return with name when seen first time - device = {"address": address, "name": name} - mock_backend.return_value.scan.return_value = [device] + mock_async_discovered_service_info.return_value = [device] config = { CONF_PLATFORM: "bluetooth_le_tracker", @@ -41,7 +100,17 @@ async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): assert result # Seen once here; return without name when seen subsequent times - device["name"] = None + device = BluetoothServiceInfo( + name=None, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] # Tick until device seen enough times for to be registered for tracking for _ in range(device_tracker.MIN_SEEN_NEW - 1): @@ -54,3 +123,193 @@ async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): state = hass.states.get(entity_id) assert state assert state.name == name + + +async def test_tracking_battery_times_out( + hass, mock_bluetooth, mock_device_tracker_conf +): + """Test tracking the battery times out.""" + + address = "DE:AD:BE:EF:13:37" + name = "Mock device name" + entity_id = f"{DOMAIN}.{slugify(name)}" + + with patch( + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + + device = BluetoothServiceInfo( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] + + config = { + CONF_PLATFORM: "bluetooth_le_tracker", + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_TRACK_BATTERY: True, + CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), + CONF_TRACK_NEW: True, + } + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + assert result + + # Tick until device seen enough times for to be registered for tracking + for _ in range(device_tracker.MIN_SEEN_NEW - 1): + async_fire_time_changed( + hass, + dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", + MockBleakClientTimesOut, + ): + # Wait for the battery scan + async_fire_time_changed( + hass, + dt_util.utcnow() + + config[CONF_SCAN_INTERVAL] + + timedelta(seconds=1) + + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.name == name + assert "battery" not in state.attributes + + +async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_conf): + """Test tracking the battery fails.""" + + address = "DE:AD:BE:EF:13:37" + name = "Mock device name" + entity_id = f"{DOMAIN}.{slugify(name)}" + + with patch( + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + + device = BluetoothServiceInfo( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] + + config = { + CONF_PLATFORM: "bluetooth_le_tracker", + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_TRACK_BATTERY: True, + CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), + CONF_TRACK_NEW: True, + } + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + assert result + + # Tick until device seen enough times for to be registered for tracking + for _ in range(device_tracker.MIN_SEEN_NEW - 1): + async_fire_time_changed( + hass, + dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", + MockBleakClientFailing, + ): + # Wait for the battery scan + async_fire_time_changed( + hass, + dt_util.utcnow() + + config[CONF_SCAN_INTERVAL] + + timedelta(seconds=1) + + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.name == name + assert "battery" not in state.attributes + + +async def test_tracking_battery_successful( + hass, mock_bluetooth, mock_device_tracker_conf +): + """Test tracking the battery gets a value.""" + + address = "DE:AD:BE:EF:13:37" + name = "Mock device name" + entity_id = f"{DOMAIN}.{slugify(name)}" + + with patch( + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + + device = BluetoothServiceInfo( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] + + config = { + CONF_PLATFORM: "bluetooth_le_tracker", + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_TRACK_BATTERY: True, + CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), + CONF_TRACK_NEW: True, + } + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + assert result + + # Tick until device seen enough times for to be registered for tracking + for _ in range(device_tracker.MIN_SEEN_NEW - 1): + async_fire_time_changed( + hass, + dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", + MockBleakClientBattery5, + ): + # Wait for the battery scan + async_fire_time_changed( + hass, + dt_util.utcnow() + + config[CONF_SCAN_INTERVAL] + + timedelta(seconds=1) + + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.name == name + assert state.attributes["battery"] == 5 From ebabaeb3648d7c35453f07be2a0f67378a733955 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 19 Jul 2022 00:29:06 +0000 Subject: [PATCH 542/820] [ci skip] Translation update --- .../components/awair/translations/ru.json | 7 ++++++ .../evil_genius_labs/translations/ca.json | 2 +- .../components/google/translations/ca.json | 3 ++- .../components/google/translations/de.json | 3 ++- .../components/google/translations/en.json | 3 ++- .../components/google/translations/et.json | 3 ++- .../components/google/translations/id.json | 3 ++- .../components/google/translations/ja.json | 3 ++- .../components/google/translations/pl.json | 3 ++- .../components/google/translations/pt-BR.json | 3 ++- .../components/google/translations/ru.json | 1 + .../components/hive/translations/ru.json | 7 ++++++ .../homekit_controller/translations/de.json | 4 ++-- .../homekit_controller/translations/id.json | 4 ++-- .../homekit_controller/translations/pl.json | 4 ++-- .../translations/pt-BR.json | 4 ++-- .../components/lcn/translations/ru.json | 1 + .../components/lifx/translations/en.json | 6 ++++- .../components/lifx/translations/pt-BR.json | 20 +++++++++++++++++ .../components/nest/translations/ru.json | 4 ++++ .../components/nina/translations/ru.json | 22 +++++++++++++++++++ .../overkiz/translations/sensor.ru.json | 5 +++++ .../simplepush/translations/ru.json | 21 ++++++++++++++++++ .../transmission/translations/ru.json | 10 ++++++++- .../ukraine_alarm/translations/ca.json | 2 +- .../components/verisure/translations/id.json | 6 +++-- .../components/verisure/translations/ru.json | 15 ++++++++++++- 27 files changed, 146 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/simplepush/translations/ru.json diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index 05a14ce7857..23424091565 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -17,6 +17,13 @@ }, "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." }, + "reauth_confirm": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." + }, "user": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", diff --git a/homeassistant/components/evil_genius_labs/translations/ca.json b/homeassistant/components/evil_genius_labs/translations/ca.json index aa6d7355314..21810a50f65 100644 --- a/homeassistant/components/evil_genius_labs/translations/ca.json +++ b/homeassistant/components/evil_genius_labs/translations/ca.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", + "timeout": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/google/translations/ca.json b/homeassistant/components/google/translations/ca.json index 004fcb67f46..066630df50d 100644 --- a/homeassistant/components/google/translations/ca.json +++ b/homeassistant/components/google/translations/ca.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "oauth_error": "S'han rebut dades token inv\u00e0lides.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" }, "create_entry": { "default": "Autenticaci\u00f3 exitosa" diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index 433324147dd..9900d3eb6d1 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -11,7 +11,8 @@ "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "oauth_error": "Ung\u00fcltige Token-Daten empfangen.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" }, "create_entry": { "default": "Erfolgreich authentifiziert" diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 2ef34ccc84b..1720e8c1454 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -11,7 +11,8 @@ "invalid_access_token": "Invalid access token", "missing_configuration": "The component is not configured. Please follow the documentation.", "oauth_error": "Received invalid token data.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "timeout_connect": "Timeout establishing connection" }, "create_entry": { "default": "Successfully authenticated" diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json index 1b5aff5774b..c516c9201e2 100644 --- a/homeassistant/components/google/translations/et.json +++ b/homeassistant/components/google/translations/et.json @@ -11,7 +11,8 @@ "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", "oauth_error": "Saadi sobimatud loaandmed.", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "timeout_connect": "\u00dchenduse loomise ajal\u00f6pp" }, "create_entry": { "default": "Tuvastamine \u00f5nnestus" diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index ea13f27fce5..20ed21a56be 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token akses tidak valid", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "oauth_error": "Menerima respons token yang tidak valid.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "timeout_connect": "Tenggang waktu membuat koneksi habis" }, "create_entry": { "default": "Berhasil diautentikasi" diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index e18ee9c4a02..6e2aac00c5d 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -11,7 +11,8 @@ "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "oauth_error": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json index 3f78e3e9d6f..ff7b8af3bfc 100644 --- a/homeassistant/components/google/translations/pl.json +++ b/homeassistant/components/google/translations/pl.json @@ -11,7 +11,8 @@ "invalid_access_token": "Niepoprawny token dost\u0119pu", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "oauth_error": "Otrzymano nieprawid\u0142owe dane tokena.", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia" }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index e934155c9fa..83d4bb54afa 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "oauth_error": "Dados de token recebidos inv\u00e1lidos.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" }, "create_entry": { "default": "Autenticado com sucesso" diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 51badc7226d..bb47301624f 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "code_expired": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043a\u043e\u0434\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0435\u0432\u0435\u0440\u043d\u043e, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", diff --git a/homeassistant/components/hive/translations/ru.json b/homeassistant/components/hive/translations/ru.json index 02736871d24..160f948b0a3 100644 --- a/homeassistant/components/hive/translations/ru.json +++ b/homeassistant/components/hive/translations/ru.json @@ -20,6 +20,13 @@ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 Hive \u0438\u043b\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 0000, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u0434\u0440\u0443\u0433\u043e\u0439 \u043a\u043e\u0434.", "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, + "configuration": { + "data": { + "device_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Hive.", + "title": "Hive" + }, "reauth": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 248d3871b3e..2b61efe6892 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -18,7 +18,7 @@ "unable_to_pair": "Koppeln fehltgeschlagen, bitte versuche es erneut", "unknown_error": "Das Ger\u00e4t meldete einen unbekannten Fehler. Die Kopplung ist fehlgeschlagen." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Breche das Pairing auf allen Controllern ab oder versuche, das Ger\u00e4t neu zu starten, und fahre dann fort, das Pairing fortzusetzen.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Pairing mit unsicheren Setup-Codes zulassen.", "pairing_code": "Kopplungscode" }, - "description": "HomeKit Controller kommuniziert mit {name} \u00fcber das lokale Netzwerk mit einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Gib deinen HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich in der Regel auf dem Ger\u00e4t selbst oder in der Verpackung.", + "description": "HomeKit Controller kommuniziert mit {name} ( {category} ) \u00fcber das lokale Netzwerk unter Verwendung einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Gib deinen HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich normalerweise auf dem Ger\u00e4t selbst oder in der Verpackung.", "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/id.json b/homeassistant/components/homekit_controller/translations/id.json index 57754bea50b..0514fcca4ed 100644 --- a/homeassistant/components/homekit_controller/translations/id.json +++ b/homeassistant/components/homekit_controller/translations/id.json @@ -18,7 +18,7 @@ "unable_to_pair": "Gagal memasangkan, coba lagi.", "unknown_error": "Perangkat melaporkan kesalahan yang tidak diketahui. Pemasangan gagal." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Batalkan pemasangan di semua pengontrol, atau coba mulai ulang perangkat, lalu lanjutkan untuk melanjutkan pemasangan.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Izinkan pemasangan dengan kode penyiapan yang tidak aman.", "pairing_code": "Kode Pemasangan" }, - "description": "Pengontrol HomeKit berkomunikasi dengan {name} melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Masukkan kode pemasangan HomeKit Anda (dalam format XXX-XX-XXX) untuk menggunakan aksesori ini. Kode ini biasanya ditemukan pada perangkat itu sendiri atau dalam kemasan.", + "description": "Pengontrol HomeKit berkomunikasi dengan {name} ({category}) melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Masukkan kode pemasangan HomeKit Anda (dalam format XXX-XX-XXX) untuk menggunakan aksesori ini. Kode ini biasanya ditemukan pada perangkat itu sendiri atau dalam kemasan.", "title": "Pasangkan dengan perangkat melalui HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index d7b5cc69cf3..d7aba3a17c7 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -18,7 +18,7 @@ "unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie", "unknown_error": "Urz\u0105dzenie zg\u0142osi\u0142o nieznany b\u0142\u0105d. Parowanie nie powiod\u0142o si\u0119." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Przerwij parowanie we wszystkich kontrolerach lub spr\u00f3buj ponownie uruchomi\u0107 urz\u0105dzenie, a nast\u0119pnie wzn\u00f3w parowanie", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Zezwalaj na parowanie z niezabezpieczonymi kodami konfiguracji.", "pairing_code": "Kod parowania" }, - "description": "Kontroler HomeKit komunikuje si\u0119 z {name} poprzez sie\u0107 lokaln\u0105 za pomoc\u0105 bezpiecznego, szyfrowanego po\u0142\u0105czenia bez oddzielnego kontrolera HomeKit lub iCloud. Wprowad\u017a kod parowania (w formacie XXX-XX-XXX), aby u\u017cy\u0107 tego akcesorium. Ten kod zazwyczaj znajduje si\u0119 na samym urz\u0105dzeniu lub w jego opakowaniu.", + "description": "Kontroler HomeKit komunikuje si\u0119 z {name} ({category}) poprzez sie\u0107 lokaln\u0105 za pomoc\u0105 bezpiecznego, szyfrowanego po\u0142\u0105czenia bez oddzielnego kontrolera HomeKit lub iCloud. Wprowad\u017a kod parowania (w formacie XXX-XX-XXX), aby u\u017cy\u0107 tego akcesorium. Ten kod zazwyczaj znajduje si\u0119 na samym urz\u0105dzeniu lub w jego opakowaniu.", "title": "Sparuj z urz\u0105dzeniem poprzez akcesorium HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/pt-BR.json b/homeassistant/components/homekit_controller/translations/pt-BR.json index 67b89e51b08..f84ff6bac37 100644 --- a/homeassistant/components/homekit_controller/translations/pt-BR.json +++ b/homeassistant/components/homekit_controller/translations/pt-BR.json @@ -18,7 +18,7 @@ "unable_to_pair": "N\u00e3o \u00e9 poss\u00edvel parear, tente novamente.", "unknown_error": "O dispositivo relatou um erro desconhecido. O pareamento falhou." }, - "flow_title": "{name}", + "flow_title": "{name} ( {category} )", "step": { "busy_error": { "description": "Abortar o emparelhamento em todos os controladores, ou tentar reiniciar o dispositivo, em seguida, continuar a retomar o emparelhamento.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Permitir o emparelhamento com c\u00f3digos de configura\u00e7\u00e3o inseguros.", "pairing_code": "C\u00f3digo de pareamento" }, - "description": "O HomeKit Controller se comunica com {name} sobre a rede local usando uma conex\u00e3o criptografada segura sem um controlador HomeKit separado ou iCloud. Digite seu c\u00f3digo de emparelhamento HomeKit (no formato XXX-XX-XXX) para usar este acess\u00f3rio. Este c\u00f3digo geralmente \u00e9 encontrado no pr\u00f3prio dispositivo ou na embalagem.", + "description": "O Controlador HomeKit se comunica com {name} ( {category} ) pela rede local usando uma conex\u00e3o criptografada segura sem um controlador HomeKit separado ou iCloud. Insira o c\u00f3digo de pareamento do HomeKit (no formato XXX-XX-XXX) para usar este acess\u00f3rio. Esse c\u00f3digo geralmente \u00e9 encontrado no pr\u00f3prio dispositivo ou na embalagem.", "title": "Emparelhar com um dispositivo atrav\u00e9s do protocolo `HomeKit Accessory`" }, "protocol_error": { diff --git a/homeassistant/components/lcn/translations/ru.json b/homeassistant/components/lcn/translations/ru.json index 0953ee96ef7..7f198d6571f 100644 --- a/homeassistant/components/lcn/translations/ru.json +++ b/homeassistant/components/lcn/translations/ru.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438", "fingerprint": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u043e\u0442\u043f\u0435\u0447\u0430\u0442\u043a\u0430 \u043f\u0430\u043b\u044c\u0446\u0430", "send_keys": "\u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u043a\u043b\u044e\u0447\u0435\u0439", "transmitter": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u0447\u0438\u043a\u0430", diff --git a/homeassistant/components/lifx/translations/en.json b/homeassistant/components/lifx/translations/en.json index 119259457a7..1f7cf981f5d 100644 --- a/homeassistant/components/lifx/translations/en.json +++ b/homeassistant/components/lifx/translations/en.json @@ -3,13 +3,17 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "no_devices_found": "No devices found on the network" + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "cannot_connect": "Failed to connect" }, "flow_title": "{label} ({host}) {serial}", "step": { + "confirm": { + "description": "Do you want to set up LIFX?" + }, "discovery_confirm": { "description": "Do you want to setup {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/pt-BR.json b/homeassistant/components/lifx/translations/pt-BR.json index f67284d8b5d..ee340af857e 100644 --- a/homeassistant/components/lifx/translations/pt-BR.json +++ b/homeassistant/components/lifx/translations/pt-BR.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "flow_title": "{label} ( {host} ) {serial}", "step": { "confirm": { "description": "Voc\u00ea quer configurar o LIFX?" + }, + "discovery_confirm": { + "description": "Deseja configurar {label} ( {host} ) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para localizar dispositivos." } } } diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index f17f48b4560..9662d4175fd 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", @@ -29,6 +30,9 @@ "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Google. \n\n\u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0442\u043e\u043a\u0435\u043d.", "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 Google" }, + "device_project_upgrade": { + "title": "Nest: \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c" + }, "init": { "data": { "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" diff --git a/homeassistant/components/nina/translations/ru.json b/homeassistant/components/nina/translations/ru.json index 114f9d44040..461637eabfc 100644 --- a/homeassistant/components/nina/translations/ru.json +++ b/homeassistant/components/nina/translations/ru.json @@ -23,5 +23,27 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433" } } + }, + "options": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "no_selection": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0445\u043e\u0442\u044f \u0431\u044b \u043e\u0434\u0438\u043d \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (A-D)", + "_e_to_h": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (E-H)", + "_i_to_l": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (I-L)", + "_m_to_q": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (M-Q)", + "_r_to_u": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (R-U)", + "_v_to_z": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (V-Z)", + "corona_filter": "\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f \u043e \u043a\u043e\u0440\u043e\u043d\u0430\u0432\u0438\u0440\u0443\u0441\u0435", + "slots": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0439 \u043d\u0430 \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.ru.json b/homeassistant/components/overkiz/translations/sensor.ru.json index afd43899b93..94adceab671 100644 --- a/homeassistant/components/overkiz/translations/sensor.ru.json +++ b/homeassistant/components/overkiz/translations/sensor.ru.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "\u0427\u0438\u0441\u0442\u043e", "dirty": "\u0413\u0440\u044f\u0437\u043d\u043e" + }, + "overkiz__three_way_handle_direction": { + "closed": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", + "tilt": "\u041d\u0430\u043a\u043b\u043e\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json new file mode 100644 index 00000000000..4844f358c82 --- /dev/null +++ b/homeassistant/components/simplepush/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "device_key": "\u041a\u043b\u044e\u0447 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "event": "\u0421\u043e\u0431\u044b\u0442\u0438\u0435 \u0434\u043b\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u0412\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c", + "salt": "\u0421\u043e\u043b\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index a83e19da400..a01c71898f8 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +10,13 @@ "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/ukraine_alarm/translations/ca.json b/homeassistant/components/ukraine_alarm/translations/ca.json index ea03b64dd3b..0c5a5e3dedb 100644 --- a/homeassistant/components/ukraine_alarm/translations/ca.json +++ b/homeassistant/components/ukraine_alarm/translations/ca.json @@ -5,7 +5,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "max_regions": "Es poden configurar un m\u00e0xim de 5 regions", "rate_limit": "Massa peticions", - "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", + "timeout": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/verisure/translations/id.json b/homeassistant/components/verisure/translations/id.json index c8b78a7282a..7f8dbe66817 100644 --- a/homeassistant/components/verisure/translations/id.json +++ b/homeassistant/components/verisure/translations/id.json @@ -18,7 +18,8 @@ }, "mfa": { "data": { - "code": "Kode Verifikasi" + "code": "Kode Verifikasi", + "description": "Verifikasi 2 langka tetap diaktifkan pada akun Anda. Masukkan kode verifikasi yang dikirimkan Verisure kepada Anda." } }, "reauth_confirm": { @@ -30,7 +31,8 @@ }, "reauth_mfa": { "data": { - "code": "Kode Verifikasi" + "code": "Kode Verifikasi", + "description": "Verifikasi 2 langka tetap diaktifkan pada akun Anda. Masukkan kode verifikasi yang dikirimkan Verisure kepada Anda." } }, "user": { diff --git a/homeassistant/components/verisure/translations/ru.json b/homeassistant/components/verisure/translations/ru.json index 430b8d773d0..c3380b48f9f 100644 --- a/homeassistant/components/verisure/translations/ru.json +++ b/homeassistant/components/verisure/translations/ru.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unknown_mfa": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 MFA \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043e\u043a Verisure \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041c\u043e\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b\u00bb \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 Home Assistant." }, + "mfa": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f", + "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0434\u0432\u0443\u0445\u044d\u0442\u0430\u043f\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 Verisure \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442 \u0412\u0430\u043c." + } + }, "reauth_confirm": { "data": { "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.", @@ -22,6 +29,12 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c" } }, + "reauth_mfa": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f", + "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0434\u0432\u0443\u0445\u044d\u0442\u0430\u043f\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 Verisure \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442 \u0412\u0430\u043c." + } + }, "user": { "data": { "description": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.", From a9e9d7b112b3555484e7a948bc995ab748a6e28d Mon Sep 17 00:00:00 2001 From: R0nd Date: Tue, 19 Jul 2022 07:13:12 +0300 Subject: [PATCH 543/820] Pass context to shopping list events (#75377) --- .../components/shopping_list/__init__.py | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 5f6a13e8e13..2af54722739 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -335,7 +335,11 @@ class ClearCompletedItemsView(http.HomeAssistantView): @callback -def websocket_handle_items(hass, connection, msg): +def websocket_handle_items( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle get shopping_list items.""" connection.send_message( websocket_api.result_message(msg["id"], hass.data[DOMAIN].items) @@ -343,15 +347,25 @@ def websocket_handle_items(hass, connection, msg): @websocket_api.async_response -async def websocket_handle_add(hass, connection, msg): +async def websocket_handle_add( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle add item to shopping_list.""" item = await hass.data[DOMAIN].async_add(msg["name"]) - hass.bus.async_fire(EVENT, {"action": "add", "item": item}) + hass.bus.async_fire( + EVENT, {"action": "add", "item": item}, context=connection.context(msg) + ) connection.send_message(websocket_api.result_message(msg["id"], item)) @websocket_api.async_response -async def websocket_handle_update(hass, connection, msg): +async def websocket_handle_update( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle update shopping_list item.""" msg_id = msg.pop("id") item_id = msg.pop("item_id") @@ -360,7 +374,9 @@ async def websocket_handle_update(hass, connection, msg): try: item = await hass.data[DOMAIN].async_update(item_id, data) - hass.bus.async_fire(EVENT, {"action": "update", "item": item}) + hass.bus.async_fire( + EVENT, {"action": "update", "item": item}, context=connection.context(msg) + ) connection.send_message(websocket_api.result_message(msg_id, item)) except KeyError: connection.send_message( @@ -369,10 +385,14 @@ async def websocket_handle_update(hass, connection, msg): @websocket_api.async_response -async def websocket_handle_clear(hass, connection, msg): +async def websocket_handle_clear( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle clearing shopping_list items.""" await hass.data[DOMAIN].async_clear_completed() - hass.bus.async_fire(EVENT, {"action": "clear"}) + hass.bus.async_fire(EVENT, {"action": "clear"}, context=connection.context(msg)) connection.send_message(websocket_api.result_message(msg["id"])) @@ -382,12 +402,18 @@ async def websocket_handle_clear(hass, connection, msg): vol.Required("item_ids"): [str], } ) -def websocket_handle_reorder(hass, connection, msg): +def websocket_handle_reorder( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle reordering shopping_list items.""" msg_id = msg.pop("id") try: hass.data[DOMAIN].async_reorder(msg.pop("item_ids")) - hass.bus.async_fire(EVENT, {"action": "reorder"}) + hass.bus.async_fire( + EVENT, {"action": "reorder"}, context=connection.context(msg) + ) connection.send_result(msg_id) except KeyError: connection.send_error( From d05160a40227fc4b9b91094b6d455265201c36ec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 06:13:53 +0200 Subject: [PATCH 544/820] Remove deprecated Lyric YAML configuration (#75418) --- homeassistant/components/lyric/__init__.py | 53 +-------------- tests/components/lyric/test_config_flow.py | 75 ++++------------------ 2 files changed, 16 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index e7fe6789268..4e4eec85899 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -11,14 +11,9 @@ from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import async_timeout -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import ( @@ -28,7 +23,6 @@ from homeassistant.helpers import ( device_registry as dr, ) from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -42,53 +36,13 @@ from .api import ( ) from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Honeywell Lyric component.""" - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - - _LOGGER.warning( - "Configuration of Honeywell Lyric integration in YAML is deprecated " - "and will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Honeywell Lyric from a config entry.""" implementation = ( @@ -143,10 +97,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=timedelta(seconds=300), ) - hass.data[DOMAIN][entry.entry_id] = coordinator - # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 6eb97f03f89..7faa63c2b1b 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -1,16 +1,17 @@ """Test the Honeywell Lyric config flow.""" -import asyncio from http import HTTPStatus from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.http import CONF_BASE_URL, DOMAIN as DOMAIN_HTTP -from homeassistant.components.lyric import config_flow +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.lyric.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -21,18 +22,12 @@ CLIENT_SECRET = "5678" @pytest.fixture() async def mock_impl(hass): """Mock implementation.""" - await setup.async_setup_component(hass, "http", {}) + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() - impl = config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, - CLIENT_ID, - CLIENT_SECRET, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "cred" ) - config_flow.OAuth2FlowHandler.async_register_implementation(hass, impl) - return impl async def test_abort_if_no_configuration(hass): @@ -45,21 +40,9 @@ async def test_abort_if_no_configuration(hass): async def test_full_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, hass_client_no_auth, aioclient_mock, current_request_with_host, mock_impl ): """Check full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, - }, - DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -98,7 +81,7 @@ async def test_full_flow( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == DOMAIN + assert result["data"]["auth_implementation"] == "cred" result["data"]["token"].pop("expires_at") assert result["data"]["token"] == { @@ -116,42 +99,10 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 -async def test_abort_if_authorization_timeout( - hass, mock_impl, current_request_with_host -): - """Check Somfy authorization timeout.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - flow = config_flow.OAuth2FlowHandler() - flow.hass = hass - - with patch.object( - mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError - ): - result = await flow.async_step_user() - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "authorize_url_timeout" - - async def test_reauthentication_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, hass_client_no_auth, aioclient_mock, current_request_with_host, mock_impl ): """Test reauthentication flow.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, - }, - DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"}, - }, - ) - old_entry = MockConfigEntry( domain=DOMAIN, unique_id=DOMAIN, From e65018fb851764dd29963459b6cdeef67d30efbe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 06:14:41 +0200 Subject: [PATCH 545/820] Clean up deprecated connection class remainders (#75421) --- homeassistant/helpers/config_entry_flow.py | 17 +---------------- tests/helpers/test_config_entry_flow.py | 11 ----------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 847a90a0d1b..bf2f95c12c6 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -10,7 +10,7 @@ from homeassistant.components import onboarding from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType +from .typing import DiscoveryInfoType if TYPE_CHECKING: import asyncio @@ -173,23 +173,8 @@ def register_discovery_flow( domain: str, title: str, discovery_function: DiscoveryFunctionType[Awaitable[bool] | bool], - connection_class: str | UndefinedType = UNDEFINED, ) -> None: """Register flow for discovered integrations that not require auth.""" - if connection_class is not UNDEFINED: - _LOGGER.warning( - ( - "The %s (%s) integration is setting a connection_class" - " when calling the 'register_discovery_flow()' method in its" - " config flow. The connection class has been deprecated and will" - " be removed in a future release of Home Assistant." - " If '%s' is a custom integration, please contact the author" - " of that integration about this warning.", - ), - title, - domain, - domain, - ) class DiscoveryFlow(DiscoveryFlowHandler[Union[Awaitable[bool], bool]]): """Discovery flow handler.""" diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 63fa58851e3..a4a3e2b27e7 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -418,14 +418,3 @@ async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_ assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" - - -async def test_warning_deprecated_connection_class(hass, caplog): - """Test that we log a warning when the connection_class is used.""" - discovery_function = Mock() - with patch.dict(config_entries.HANDLERS): - config_entry_flow.register_discovery_flow( - "test", "Test", discovery_function, connection_class="local_polling" - ) - - assert "integration is setting a connection_class" in caplog.text From 24b3b5fc46fea2fa2f3caab2c6b593369cec72fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 06:14:55 +0200 Subject: [PATCH 546/820] Remove deprecated Senz YAML configuration (#75419) --- homeassistant/components/senz/__init__.py | 51 ++--------------------- tests/components/senz/test_config_flow.py | 18 ++++---- 2 files changed, 14 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 6f68189e8bb..012b40f5def 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -6,14 +6,9 @@ import logging from aiosenz import SENZAPI, Thermostat from httpx import RequestError -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( @@ -21,7 +16,6 @@ from homeassistant.helpers import ( config_validation as cv, httpx_client, ) -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .api import SENZConfigEntryAuth @@ -31,52 +25,13 @@ UPDATE_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.CLIMATE] SENZDataUpdateCoordinator = DataUpdateCoordinator[dict[str, Thermostat]] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the SENZ OAuth2 configuration.""" - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of SENZ integration in YAML is deprecated " - "and will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SENZ from a config entry.""" implementation = ( @@ -111,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/tests/components/senz/test_config_flow.py b/tests/components/senz/test_config_flow.py index 3906f2c0320..f0ed54ba932 100644 --- a/tests/components/senz/test_config_flow.py +++ b/tests/components/senz/test_config_flow.py @@ -3,10 +3,15 @@ from unittest.mock import patch from aiosenz import AUTHORIZATION_ENDPOINT, TOKEN_ENDPOINT -from homeassistant import config_entries, setup +from homeassistant import config_entries +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.senz.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -19,12 +24,11 @@ async def test_full_flow( current_request_with_host, ) -> None: """Check full flow.""" - assert await setup.async_setup_component( - hass, - "senz", - { - "senz": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, - }, + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "cred" ) result = await hass.config_entries.flow.async_init( From 8e8c6e23945fa0d66619d6488fa359714bf406d7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 06:15:22 +0200 Subject: [PATCH 547/820] Remove unused ignore file (#75416) --- .ignore | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .ignore diff --git a/.ignore b/.ignore deleted file mode 100644 index 45c6dc5561f..00000000000 --- a/.ignore +++ /dev/null @@ -1,6 +0,0 @@ -# Patterns matched in this file will be ignored by supported search utilities - -# Ignore generated html and javascript files -/homeassistant/components/frontend/www_static/*.html -/homeassistant/components/frontend/www_static/*.js -/homeassistant/components/frontend/www_static/panels/*.html From c3d536b25520bb784b8874275d2e7a91d1954169 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Jul 2022 13:01:39 +0200 Subject: [PATCH 548/820] Store creation timestamps for resolution center issues (#75430) --- .../resolution_center/issue_registry.py | 15 ++++-- .../resolution_center/websocket_api.py | 3 +- .../components/resolution_center/test_init.py | 47 ++++++++++++++++++- .../resolution_center/test_issue_registry.py | 4 ++ .../resolution_center/test_websocket_api.py | 6 +++ 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py index d9042d891dd..aed1cd51b10 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -2,11 +2,13 @@ from __future__ import annotations import dataclasses +from datetime import datetime from typing import Optional, cast from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store +import homeassistant.util.dt as dt_util from .models import IssueSeverity @@ -14,7 +16,6 @@ DATA_REGISTRY = "issue_registry" STORAGE_KEY = "resolution_center.issue_registry" STORAGE_VERSION = 1 SAVE_DELAY = 10 -SAVED_FIELDS = ("dismissed_version", "domain", "issue_id") @dataclasses.dataclass(frozen=True) @@ -23,6 +24,7 @@ class IssueEntry: active: bool breaks_in_ha_version: str | None + created: datetime dismissed_version: str | None domain: str is_fixable: bool | None @@ -68,6 +70,7 @@ class IssueRegistry: issue = IssueEntry( active=True, breaks_in_ha_version=breaks_in_ha_version, + created=dt_util.utcnow(), dismissed_version=None, domain=domain, is_fixable=is_fixable, @@ -125,10 +128,11 @@ class IssueRegistry: if isinstance(data, dict): for issue in data["issues"]: - assert issue["domain"] and issue["issue_id"] + assert issue["created"] and issue["domain"] and issue["issue_id"] issues[(issue["domain"], issue["issue_id"])] = IssueEntry( active=False, breaks_in_ha_version=None, + created=cast(datetime, dt_util.parse_datetime(issue["created"])), dismissed_version=issue["dismissed_version"], domain=issue["domain"], is_fixable=None, @@ -152,7 +156,12 @@ class IssueRegistry: data = {} data["issues"] = [ - {field: getattr(entry, field) for field in SAVED_FIELDS} + { + "created": entry.created.isoformat(), + "dismissed_version": entry.dismissed_version, + "domain": entry.domain, + "issue_id": entry.issue_id, + } for entry in self.issues.values() ] diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py index dfa4f1903d9..314df009118 100644 --- a/homeassistant/components/resolution_center/websocket_api.py +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -67,8 +67,9 @@ def ws_list_issues( """Return a list of issues.""" def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: - result = {k: v for k, v in kv_pairs if k != "active"} + result = {k: v for k, v in kv_pairs if k not in ("active")} result["dismissed"] = result["dismissed_version"] is not None + result["created"] = result["created"].isoformat() return result issue_registry = async_get_issue_registry(hass) diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py index 66f9bc42935..a707c3845fe 100644 --- a/tests/components/resolution_center/test_init.py +++ b/tests/components/resolution_center/test_init.py @@ -1,6 +1,7 @@ """Test the resolution center websocket API.""" from unittest.mock import AsyncMock, Mock +from freezegun import freeze_time import pytest from homeassistant.components.resolution_center import ( @@ -19,6 +20,7 @@ from homeassistant.setup import async_setup_component from tests.common import mock_platform +@freeze_time("2022-07-19 07:53:05") async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: """Test creating and updating issues.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -75,6 +77,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -101,6 +104,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"]["issues"][0] == dict( issues[0], + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, learn_more_url="blablabla", @@ -147,6 +151,7 @@ async def test_create_issue_invalid_version( assert msg["result"] == {"issues": []} +@freeze_time("2022-07-19 07:53:05") async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: """Test dismissing issues.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -193,6 +198,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -212,6 +218,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -230,6 +237,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=True, dismissed_version=ha_version, ) @@ -248,6 +256,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=True, dismissed_version=ha_version, ) @@ -274,14 +283,16 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"]["issues"][0] == dict( issues[0], + created="2022-07-19T07:53:05+00:00", dismissed=True, dismissed_version=ha_version, learn_more_url="blablabla", ) -async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: +async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> None: """Test we can delete an issue.""" + freezer.move_to("2022-07-19 07:53:05") assert await async_setup_component(hass, DOMAIN, {}) client = await hass_ws_client(hass) @@ -320,6 +331,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -338,6 +350,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -363,6 +376,38 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"] == {"issues": []} + # Create the same issues again created timestamp should change + freezer.move_to("2022-07-19 08:53:05") + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T08:53:05+00:00", + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> None: """Test non-compliant platforms are not registered.""" diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py index a0dffaacc8f..3f5cc235a19 100644 --- a/tests/components/resolution_center/test_issue_registry.py +++ b/tests/components/resolution_center/test_issue_registry.py @@ -64,8 +64,10 @@ async def test_load_issues(hass: HomeAssistant) -> None: assert list(registry.issues) == list(registry2.issues) issue1_registry2 = registry2.async_get_issue("test", "issue_1") + assert issue1_registry2.created == issue1.created assert issue1_registry2.dismissed_version == issue1.dismissed_version issue2_registry2 = registry2.async_get_issue("test", "issue_2") + assert issue2_registry2.created == issue2.created assert issue2_registry2.dismissed_version == issue2.dismissed_version @@ -76,11 +78,13 @@ async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> "data": { "issues": [ { + "created": "2022-07-19T09:41:13.746514+00:00", "dismissed_version": "2022.7.0.dev0", "domain": "test", "issue_id": "issue_1", }, { + "created": "2022-07-19T19:41:13.746514+00:00", "dismissed_version": None, "domain": "test", "issue_id": "issue_2", diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py index 42899065121..8701996a535 100644 --- a/tests/components/resolution_center/test_websocket_api.py +++ b/tests/components/resolution_center/test_websocket_api.py @@ -4,6 +4,7 @@ from __future__ import annotations from http import HTTPStatus from unittest.mock import ANY, AsyncMock, Mock +from freezegun import freeze_time import pytest import voluptuous as vol @@ -56,6 +57,7 @@ async def create_issues(hass, ws_client): "issues": [ dict( issue, + created=ANY, dismissed=False, dismissed_version=None, ) @@ -146,6 +148,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created=ANY, dismissed=True, dismissed_version=ha_version, ) @@ -188,6 +191,7 @@ async def test_fix_non_existing_issue( "issues": [ dict( issue, + created=ANY, dismissed=False, dismissed_version=None, ) @@ -333,6 +337,7 @@ async def test_step_unauth( assert resp.status == HTTPStatus.UNAUTHORIZED +@freeze_time("2022-07-19 07:53:05") async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: """Test we can list issues.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -389,6 +394,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) From 403bbda9592a19ef1f2490de0cc55799256eaf31 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Jul 2022 13:58:39 +0200 Subject: [PATCH 549/820] Rename resolution_center dismiss to ignore (#75432) --- .../resolution_center/issue_handler.py | 8 ++- .../resolution_center/issue_registry.py | 9 +-- .../resolution_center/websocket_api.py | 13 ++-- .../components/resolution_center/test_init.py | 60 ++++++++++++------- .../resolution_center/test_issue_registry.py | 4 +- .../resolution_center/test_websocket_api.py | 43 +++++++++++-- 6 files changed, 96 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/resolution_center/issue_handler.py index 210a998ce14..78085e2cec6 100644 --- a/homeassistant/components/resolution_center/issue_handler.py +++ b/homeassistant/components/resolution_center/issue_handler.py @@ -128,10 +128,12 @@ def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: @callback -def async_dismiss_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: - """Dismiss an issue. +def async_ignore_issue( + hass: HomeAssistant, domain: str, issue_id: str, ignore: bool +) -> None: + """Ignore an issue. Will raise if the issue does not exist. """ issue_registry = async_get_issue_registry(hass) - issue_registry.async_dismiss(domain, issue_id) + issue_registry.async_ignore(domain, issue_id, ignore) diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py index aed1cd51b10..e398d58b3e0 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -105,15 +105,16 @@ class IssueRegistry: self.async_schedule_save() @callback - def async_dismiss(self, domain: str, issue_id: str) -> IssueEntry: - """Dismiss issue.""" + def async_ignore(self, domain: str, issue_id: str, ignore: bool) -> IssueEntry: + """Ignore issue.""" old = self.issues[(domain, issue_id)] - if old.dismissed_version == ha_version: + dismissed_version = ha_version if ignore else None + if old.dismissed_version == dismissed_version: return old issue = self.issues[(domain, issue_id)] = dataclasses.replace( old, - dismissed_version=ha_version, + dismissed_version=dismissed_version, ) self.async_schedule_save() diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py index 314df009118..e111c2b3e50 100644 --- a/homeassistant/components/resolution_center/websocket_api.py +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -20,14 +20,14 @@ from homeassistant.helpers.data_entry_flow import ( ) from .const import DOMAIN -from .issue_handler import async_dismiss_issue +from .issue_handler import async_ignore_issue from .issue_registry import async_get as async_get_issue_registry @callback def async_setup(hass: HomeAssistant) -> None: """Set up the resolution center websocket API.""" - websocket_api.async_register_command(hass, ws_dismiss_issue) + websocket_api.async_register_command(hass, ws_ignore_issue) websocket_api.async_register_command(hass, ws_list_issues) hass.http.register_view( @@ -41,16 +41,17 @@ def async_setup(hass: HomeAssistant) -> None: @callback @websocket_api.websocket_command( { - vol.Required("type"): "resolution_center/dismiss_issue", + vol.Required("type"): "resolution_center/ignore_issue", vol.Required("domain"): str, vol.Required("issue_id"): str, + vol.Required("ignore"): bool, } ) -def ws_dismiss_issue( +def ws_ignore_issue( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fix an issue.""" - async_dismiss_issue(hass, msg["domain"], msg["issue_id"]) + async_ignore_issue(hass, msg["domain"], msg["issue_id"], msg["ignore"]) connection.send_result(msg["id"]) @@ -68,7 +69,7 @@ def ws_list_issues( def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: result = {k: v for k, v in kv_pairs if k not in ("active")} - result["dismissed"] = result["dismissed_version"] is not None + result["ignored"] = result["dismissed_version"] is not None result["created"] = result["created"].isoformat() return result diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py index a707c3845fe..478c2d51f7f 100644 --- a/tests/components/resolution_center/test_init.py +++ b/tests/components/resolution_center/test_init.py @@ -10,7 +10,7 @@ from homeassistant.components.resolution_center import ( ) from homeassistant.components.resolution_center.const import DOMAIN from homeassistant.components.resolution_center.issue_handler import ( - async_dismiss_issue, + async_ignore_issue, async_process_resolution_center_platforms, ) from homeassistant.const import __version__ as ha_version @@ -78,8 +78,8 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -105,8 +105,8 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["result"]["issues"][0] == dict( issues[0], created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, learn_more_url="blablabla", ) @@ -152,8 +152,8 @@ async def test_create_issue_invalid_version( @freeze_time("2022-07-19 07:53:05") -async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: - """Test dismissing issues.""" +async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test ignoring issues.""" assert await async_setup_component(hass, DOMAIN, {}) client = await hass_ws_client(hass) @@ -199,16 +199,16 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] } - # Dismiss a non-existing issue + # Ignore a non-existing issue with pytest.raises(KeyError): - async_dismiss_issue(hass, issues[0]["domain"], "no_such_issue") + async_ignore_issue(hass, issues[0]["domain"], "no_such_issue", True) await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) msg = await client.receive_json() @@ -219,15 +219,15 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] } - # Dismiss an existing issue - async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + # Ignore an existing issue + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) msg = await client.receive_json() @@ -238,15 +238,15 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=True, dismissed_version=ha_version, + ignored=True, ) for issue in issues ] } - # Dismiss the same issue again - async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + # Ignore the same issue again + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) msg = await client.receive_json() @@ -257,14 +257,14 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=True, dismissed_version=ha_version, + ignored=True, ) for issue in issues ] } - # Update a dismissed issue + # Update an ignored issue async_create_issue( hass, issues[0]["domain"], @@ -284,11 +284,31 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["result"]["issues"][0] == dict( issues[0], created="2022-07-19T07:53:05+00:00", - dismissed=True, dismissed_version=ha_version, + ignored=True, learn_more_url="blablabla", ) + # Unignore the same issue + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], False) + + await client.send_json({"id": 7, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + learn_more_url="blablabla", + ) + for issue in issues + ] + } + async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> None: """Test we can delete an issue.""" @@ -332,8 +352,8 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -351,8 +371,8 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -401,8 +421,8 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non dict( issue, created="2022-07-19T08:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py index 3f5cc235a19..854a14fc84e 100644 --- a/tests/components/resolution_center/test_issue_registry.py +++ b/tests/components/resolution_center/test_issue_registry.py @@ -4,7 +4,7 @@ from homeassistant.components.resolution_center import ( issue_registry, ) from homeassistant.components.resolution_center.const import DOMAIN -from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.components.resolution_center.issue_handler import async_ignore_issue from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -50,7 +50,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: translation_key=issue["translation_key"], translation_placeholders=issue["translation_placeholders"], ) - async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] assert len(registry.issues) == 2 diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py index 8701996a535..044b0e9832b 100644 --- a/tests/components/resolution_center/test_websocket_api.py +++ b/tests/components/resolution_center/test_websocket_api.py @@ -58,8 +58,8 @@ async def create_issues(hass, ws_client): dict( issue, created=ANY, - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -120,9 +120,10 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 2, - "type": "resolution_center/dismiss_issue", + "type": "resolution_center/ignore_issue", "domain": "fake_integration", "issue_id": "no_such_issue", + "ignore": True, } ) msg = await client.receive_json() @@ -131,9 +132,10 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 3, - "type": "resolution_center/dismiss_issue", + "type": "resolution_center/ignore_issue", "domain": "fake_integration", "issue_id": "issue_1", + "ignore": True, } ) msg = await client.receive_json() @@ -149,8 +151,37 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created=ANY, - dismissed=True, dismissed_version=ha_version, + ignored=True, + ) + for issue in issues + ] + } + + await client.send_json( + { + "id": 5, + "type": "resolution_center/ignore_issue", + "domain": "fake_integration", + "issue_id": "issue_1", + "ignore": False, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + await client.send_json({"id": 6, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created=ANY, + dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -192,8 +223,8 @@ async def test_fix_non_existing_issue( dict( issue, created=ANY, - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -395,8 +426,8 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] From 25b874a6092f463694a12b8de805f4aacc7c5d9e Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 19 Jul 2022 20:05:28 +0800 Subject: [PATCH 550/820] Fix yolink leak sensor battery expose (#75423) --- homeassistant/components/yolink/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 4679c3e670b..6a7c7ea4cff 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -52,6 +52,7 @@ class YoLinkSensorEntityDescription( SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, From b6d235c0c2c9755f1c54d35808da2307faeeb18d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 15:21:17 +0200 Subject: [PATCH 551/820] Improve tradfri decorator typing (#75439) --- homeassistant/components/tradfri/base_class.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index bd1968dfd15..a2b7304cc3e 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Callable +from collections.abc import Callable, Coroutine from functools import wraps from typing import Any, cast @@ -20,7 +20,7 @@ from .coordinator import TradfriDeviceDataUpdateCoordinator def handle_error( func: Callable[[Command | list[Command]], Any] -) -> Callable[[str], Any]: +) -> Callable[[Command | list[Command]], Coroutine[Any, Any, None]]: """Handle tradfri api call error.""" @wraps(func) From 6b60fb954197003c05789a6f34e91d2989e091eb Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 19 Jul 2022 21:40:23 +0800 Subject: [PATCH 552/820] Don't use executor in send_big_result (#75427) --- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/lovelace/websocket.py | 2 +- homeassistant/components/media_player/__init__.py | 2 +- homeassistant/components/websocket_api/connection.py | 7 ++----- tests/components/websocket_api/test_connection.py | 4 ++-- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 45b77ec1bd6..35fa6fef1d6 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -804,7 +804,7 @@ async def websocket_camera_thumbnail( _LOGGER.warning("The websocket command 'camera_thumbnail' has been deprecated") try: image = await async_get_image(hass, msg["entity_id"]) - await connection.send_big_result( + connection.send_big_result( msg["id"], { "content_type": image.content_type, diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index cb45d9cfbc6..7e04201291d 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -38,7 +38,7 @@ def _handle_errors(func): return if msg is not None: - await connection.send_big_result(msg["id"], result) + connection.send_big_result(msg["id"], result) else: connection.send_result(msg["id"], result) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 14546a36ec8..5e5347e6806 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1164,7 +1164,7 @@ async def websocket_handle_thumbnail(hass, connection, msg): ) return - await connection.send_big_result( + connection.send_big_result( msg["id"], { "content_type": content_type, diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 26c4c6f8321..1c26d958969 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -54,12 +54,9 @@ class ActiveConnection: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) - async def send_big_result(self, msg_id: int, result: Any) -> None: + def send_big_result(self, msg_id: int, result: Any) -> None: """Send a result message that would be expensive to JSON serialize.""" - content = await self.hass.async_add_executor_job( - JSON_DUMP, messages.result_message(msg_id, result) - ) - self.send_message(content) + self.send_message(JSON_DUMP(messages.result_message(msg_id, result))) @callback def send_error(self, msg_id: int, code: str, message: str) -> None: diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 3a54d5912e0..da31f0ee8a3 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -17,8 +17,8 @@ async def test_send_big_result(hass, websocket_client): @websocket_api.websocket_command({"type": "big_result"}) @websocket_api.async_response - async def send_big_result(hass, connection, msg): - await connection.send_big_result(msg["id"], {"big": "result"}) + def send_big_result(hass, connection, msg): + connection.send_big_result(msg["id"], {"big": "result"}) websocket_api.async_register_command(hass, send_big_result) From 4b036cbad91c4e6a8eba6952af4a01d39a22a4e2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 18:33:53 +0200 Subject: [PATCH 553/820] Add typing to pilight Throttle decorator (#75443) --- homeassistant/components/pilight/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 38d7a3e18ea..0386d267f04 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -1,11 +1,16 @@ """Component to create an interface to a Pilight daemon.""" +from __future__ import annotations + +from collections.abc import Callable from datetime import timedelta import functools import logging import socket import threading +from typing import Any from pilight import pilight +from typing_extensions import ParamSpec import voluptuous as vol from homeassistant.const import ( @@ -22,6 +27,8 @@ from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util +_P = ParamSpec("_P") + _LOGGER = logging.getLogger(__name__) CONF_SEND_DELAY = "send_delay" @@ -138,23 +145,23 @@ class CallRateDelayThrottle: def __init__(self, hass, delay_seconds: float) -> None: """Initialize the delay handler.""" self._delay = timedelta(seconds=max(0.0, delay_seconds)) - self._queue: list = [] + self._queue: list[Callable[[Any], None]] = [] self._active = False self._lock = threading.Lock() self._next_ts = dt_util.utcnow() self._schedule = functools.partial(track_point_in_utc_time, hass) - def limited(self, method): + def limited(self, method: Callable[_P, Any]) -> Callable[_P, None]: """Decorate to delay calls on a certain method.""" @functools.wraps(method) - def decorated(*args, **kwargs): + def decorated(*args: _P.args, **kwargs: _P.kwargs) -> None: """Delay a call.""" if self._delay.total_seconds() == 0.0: method(*args, **kwargs) return - def action(event): + def action(event: Any) -> None: """Wrap an action that gets scheduled.""" method(*args, **kwargs) From 5ae5ae5392729b4c94a8004bd02e147d60227341 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 18:35:04 +0200 Subject: [PATCH 554/820] Improve debouncer typing (#75436) --- .strict-typing | 1 + homeassistant/components/flux_led/number.py | 5 +++-- homeassistant/components/plex/__init__.py | 2 +- homeassistant/components/samsungtv/__init__.py | 4 ++-- homeassistant/components/shelly/__init__.py | 5 +++-- .../components/sonos/household_coordinator.py | 3 ++- homeassistant/components/usb/__init__.py | 5 +++-- homeassistant/helpers/debounce.py | 18 +++++++++++------- homeassistant/helpers/device_registry.py | 3 ++- homeassistant/helpers/update_coordinator.py | 4 ++-- mypy.ini | 3 +++ 11 files changed, 33 insertions(+), 20 deletions(-) diff --git a/.strict-typing b/.strict-typing index aa911dd81d9..f133199a753 100644 --- a/.strict-typing +++ b/.strict-typing @@ -15,6 +15,7 @@ homeassistant.auth.auth_store homeassistant.auth.providers.* homeassistant.helpers.area_registry homeassistant.helpers.condition +homeassistant.helpers.debounce homeassistant.helpers.discovery homeassistant.helpers.entity homeassistant.helpers.entity_values diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 65c8a955dcf..18237c97e94 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -2,8 +2,9 @@ from __future__ import annotations from abc import abstractmethod +from collections.abc import Coroutine import logging -from typing import cast +from typing import Any, cast from flux_led.protocol import ( MUSIC_PIXELS_MAX, @@ -143,7 +144,7 @@ class FluxConfigNumber( ) -> None: """Initialize the flux number.""" super().__init__(coordinator, base_unique_id, name, key) - self._debouncer: Debouncer | None = None + self._debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None self._pending_value: int | None = None async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 148e6a906bd..9ff8bcf7b54 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -95,7 +95,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("Scanning for GDM clients") gdm.scan(scan_for_clients=True) - hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer( + hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer[None]( hass, _LOGGER, cooldown=10, diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index b870aab62d4..0a7fd4b3378 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1,7 +1,7 @@ """The Samsung TV integration.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Coroutine, Mapping from functools import partial import socket from typing import Any @@ -131,7 +131,7 @@ class DebouncedEntryReloader: self.hass = hass self.entry = entry self.token = self.entry.data.get(CONF_TOKEN) - self._debounced_reload = Debouncer( + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 012f692c579..125e63449ef 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine from datetime import timedelta from typing import Any, Final, cast @@ -296,7 +297,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device - self._debounced_reload = Debouncer( + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, @@ -636,7 +637,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device - self._debounced_reload = Debouncer( + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py index 0d76feae461..51d7e9cec8c 100644 --- a/homeassistant/components/sonos/household_coordinator.py +++ b/homeassistant/components/sonos/household_coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine import logging +from typing import Any from soco import SoCo @@ -35,7 +36,7 @@ class SonosHouseholdCoordinator: async def _async_setup(self) -> None: """Finish setup in async context.""" self.cache_update_lock = asyncio.Lock() - self.async_poll = Debouncer( + self.async_poll = Debouncer[Coroutine[Any, Any, None]]( self.hass, _LOGGER, cooldown=3, diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 7a56a659d07..5783401df13 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -1,12 +1,13 @@ """The USB Discovery integration.""" from __future__ import annotations +from collections.abc import Coroutine import dataclasses import fnmatch import logging import os import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -109,7 +110,7 @@ class USBDiscovery: self.usb = usb self.seen: set[tuple[str, ...]] = set() self.observer_active = False - self._request_debouncer: Debouncer | None = None + self._request_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None async def async_setup(self) -> None: """Set up USB Discovery.""" diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 7937459b50c..2fbdefd7ec0 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -2,14 +2,16 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Callable from logging import Logger -from typing import Any +from typing import Generic, TypeVar from homeassistant.core import HassJob, HomeAssistant, callback +_R_co = TypeVar("_R_co", covariant=True) -class Debouncer: + +class Debouncer(Generic[_R_co]): """Class to rate limit calls to a specific command.""" def __init__( @@ -19,7 +21,7 @@ class Debouncer: *, cooldown: float, immediate: bool, - function: Callable[..., Awaitable[Any]] | None = None, + function: Callable[[], _R_co] | None = None, ) -> None: """Initialize debounce. @@ -35,15 +37,17 @@ class Debouncer: self._timer_task: asyncio.TimerHandle | None = None self._execute_at_end_of_timer: bool = False self._execute_lock = asyncio.Lock() - self._job: HassJob | None = None if function is None else HassJob(function) + self._job: HassJob[[], _R_co] | None = ( + None if function is None else HassJob(function) + ) @property - def function(self) -> Callable[..., Awaitable[Any]] | None: + def function(self) -> Callable[[], _R_co] | None: """Return the function being wrapped by the Debouncer.""" return self._function @function.setter - def function(self, function: Callable[..., Awaitable[Any]]) -> None: + def function(self, function: Callable[[], _R_co]) -> None: """Update the function being wrapped by the Debouncer.""" self._function = function if self._job is None or function != self._job.target: diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index ca5e6e1aefa..a19f476495a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Coroutine import logging import time from typing import TYPE_CHECKING, Any, NamedTuple, cast @@ -832,7 +833,7 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: ent_reg = entity_registry.async_get(hass) async_cleanup(hass, dev_reg, ent_reg) - debounced_cleanup = Debouncer( + debounced_cleanup: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, _LOGGER, cooldown=CLEANUP_DELAY, immediate=False, function=cleanup ) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 30da847642b..768b8040729 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable, Coroutine, Generator from datetime import datetime, timedelta import logging from time import monotonic @@ -44,7 +44,7 @@ class DataUpdateCoordinator(Generic[_T]): name: str, update_interval: timedelta | None = None, update_method: Callable[[], Awaitable[_T]] | None = None, - request_refresh_debouncer: Debouncer | None = None, + request_refresh_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, ) -> None: """Initialize global data updater.""" self.hass = hass diff --git a/mypy.ini b/mypy.ini index 2333c20c4d8..3355680069e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -57,6 +57,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.condition] disallow_any_generics = true +[mypy-homeassistant.helpers.debounce] +disallow_any_generics = true + [mypy-homeassistant.helpers.discovery] disallow_any_generics = true From 32311f240b20548d9c619f681833f54cb833557f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Jul 2022 11:50:30 -0500 Subject: [PATCH 555/820] Avoid converting discovery_info dataclasses to dict that will be thrown away in config flows (#75451) * Avoid converting BluetoothServiceInfo to a dict for default discovery Fixes ``` 2022-07-19 09:46:48.303 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/helpers/discovery_flow.py", line 74, in _async_process_pending_flows await gather_with_concurrency( File "/Users/bdraco/home-assistant/homeassistant/util/async_.py", line 201, in gather_with_concurrency return await gather( File "/Users/bdraco/home-assistant/homeassistant/util/async_.py", line 199, in sem_task return await task File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 222, in async_init flow, result = await task File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 249, in _async_init result = await self._async_handle_step(flow, flow.init_step, data, init_done) File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 359, in _async_handle_step result: FlowResult = await getattr(flow, method)(user_input) File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 1484, in async_step_bluetooth return await self.async_step_discovery(dataclasses.asdict(discovery_info)) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1239, in asdict return _asdict_inner(obj, dict_factory) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1246, in _asdict_inner value = _asdict_inner(getattr(obj, f.name), dict_factory) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1280, in _asdict_inner return copy.deepcopy(obj) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 172, in deepcopy y = _reconstruct(x, memo, *rv) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 271, in _reconstruct state = deepcopy(state, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 146, in deepcopy y = copier(x, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 231, in _deepcopy_dict y[deepcopy(key, memo)] = deepcopy(value, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 161, in deepcopy rv = reductor(4) TypeError: Cannot pickle Objective-C objects ``` * Avoid converting BluetoothServiceInfo to a dict for default discovery Fixes ``` 2022-07-19 09:46:48.303 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/helpers/discovery_flow.py", line 74, in _async_process_pending_flows await gather_with_concurrency( File "/Users/bdraco/home-assistant/homeassistant/util/async_.py", line 201, in gather_with_concurrency return await gather( File "/Users/bdraco/home-assistant/homeassistant/util/async_.py", line 199, in sem_task return await task File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 222, in async_init flow, result = await task File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 249, in _async_init result = await self._async_handle_step(flow, flow.init_step, data, init_done) File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 359, in _async_handle_step result: FlowResult = await getattr(flow, method)(user_input) File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 1484, in async_step_bluetooth return await self.async_step_discovery(dataclasses.asdict(discovery_info)) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1239, in asdict return _asdict_inner(obj, dict_factory) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1246, in _asdict_inner value = _asdict_inner(getattr(obj, f.name), dict_factory) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1280, in _asdict_inner return copy.deepcopy(obj) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 172, in deepcopy y = _reconstruct(x, memo, *rv) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 271, in _reconstruct state = deepcopy(state, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 146, in deepcopy y = copier(x, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 231, in _deepcopy_dict y[deepcopy(key, memo)] = deepcopy(value, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 161, in deepcopy rv = reductor(4) TypeError: Cannot pickle Objective-C objects ``` --- homeassistant/config_entries.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e5a36b980ed..232d3e5cbf1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -5,7 +5,6 @@ import asyncio from collections import ChainMap from collections.abc import Callable, Coroutine, Iterable, Mapping from contextvars import ContextVar -import dataclasses from enum import Enum import functools import logging @@ -1446,13 +1445,19 @@ class ConfigFlow(data_entry_flow.FlowHandler): if self._async_in_progress(include_uninitialized=True): raise data_entry_flow.AbortFlow("already_in_progress") - async def async_step_discovery( - self, discovery_info: DiscoveryInfoType + async def _async_step_discovery_without_unique_id( + self, ) -> data_entry_flow.FlowResult: """Handle a flow initialized by discovery.""" await self._async_handle_discovery_without_unique_id() return await self.async_step_user() + async def async_step_discovery( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by discovery.""" + return await self._async_step_discovery_without_unique_id() + @callback def async_abort( self, @@ -1481,55 +1486,55 @@ class ConfigFlow(data_entry_flow.FlowHandler): self, discovery_info: BluetoothServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Bluetooth discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_dhcp( self, discovery_info: DhcpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by DHCP discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_hassio( self, discovery_info: HassioServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by HASS IO discovery.""" - return await self.async_step_discovery(discovery_info.config) + return await self._async_step_discovery_without_unique_id() async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> data_entry_flow.FlowResult: """Handle a flow initialized by integration specific discovery.""" - return await self.async_step_discovery(discovery_info) + return await self._async_step_discovery_without_unique_id() async def async_step_homekit( self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Homekit discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_mqtt( self, discovery_info: MqttServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by MQTT discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_ssdp( self, discovery_info: SsdpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by SSDP discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_usb( self, discovery_info: UsbServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by USB discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Zeroconf discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() @callback def async_create_entry( From e02a24529fa01075f0f3daa5906fac1aca71c575 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 18:53:19 +0200 Subject: [PATCH 556/820] Update mypy to 0.971 (#75450) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index adecd327631..2331e971711 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ codecov==2.1.12 coverage==6.4.2 freezegun==1.2.1 mock-open==1.4.0 -mypy==0.961 +mypy==0.971 pre-commit==2.20.0 pylint==2.14.4 pipdeptree==2.2.1 From e4f6f738e83b2b3d5e786d42c53dea867a435b29 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 19 Jul 2022 18:56:41 +0200 Subject: [PATCH 557/820] Bump python-miio to 0.5.12 (#75415) * Bump python-miio to 0.5.12 * Fix imports --- homeassistant/components/xiaomi_miio/fan.py | 16 +++++++++++---- .../components/xiaomi_miio/humidifier.py | 12 ++++++++--- .../components/xiaomi_miio/manifest.json | 2 +- .../components/xiaomi_miio/select.py | 20 ++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 39 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 177f84679ee..aa4b8a8a1bc 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -7,14 +7,22 @@ import logging import math from typing import Any -from miio.airfresh import OperationMode as AirfreshOperationMode -from miio.airfresh_t2017 import OperationMode as AirfreshOperationModeT2017 -from miio.airpurifier import OperationMode as AirpurifierOperationMode -from miio.airpurifier_miot import OperationMode as AirpurifierMiotOperationMode from miio.fan_common import ( MoveDirection as FanMoveDirection, OperationMode as FanOperationMode, ) +from miio.integrations.airpurifier.dmaker.airfresh_t2017 import ( + OperationMode as AirfreshOperationModeT2017, +) +from miio.integrations.airpurifier.zhimi.airfresh import ( + OperationMode as AirfreshOperationMode, +) +from miio.integrations.airpurifier.zhimi.airpurifier import ( + OperationMode as AirpurifierOperationMode, +) +from miio.integrations.airpurifier.zhimi.airpurifier_miot import ( + OperationMode as AirpurifierMiotOperationMode, +) from miio.integrations.fan.zhimi.zhimi_miot import ( OperationModeFanZA5 as FanZA5OperationMode, ) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 0a9543ac604..b5a5e738ea0 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -2,9 +2,15 @@ import logging import math -from miio.airhumidifier import OperationMode as AirhumidifierOperationMode -from miio.airhumidifier_miot import OperationMode as AirhumidifierMiotOperationMode -from miio.airhumidifier_mjjsq import OperationMode as AirhumidifierMjjsqOperationMode +from miio.integrations.humidifier.deerma.airhumidifier_mjjsq import ( + OperationMode as AirhumidifierMjjsqOperationMode, +) +from miio.integrations.humidifier.zhimi.airhumidifier import ( + OperationMode as AirhumidifierOperationMode, +) +from miio.integrations.humidifier.zhimi.airhumidifier_miot import ( + OperationMode as AirhumidifierMiotOperationMode, +) from homeassistant.components.humidifier import ( HumidifierDeviceClass, diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 7157e32299a..0f1a9dd92aa 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.11"], + "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.12"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling", diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 5f8fe8df591..b7e6a65775e 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -3,12 +3,22 @@ from __future__ import annotations from dataclasses import dataclass -from miio.airfresh import LedBrightness as AirfreshLedBrightness -from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness -from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness -from miio.airpurifier import LedBrightness as AirpurifierLedBrightness -from miio.airpurifier_miot import LedBrightness as AirpurifierMiotLedBrightness from miio.fan_common import LedBrightness as FanLedBrightness +from miio.integrations.airpurifier.zhimi.airfresh import ( + LedBrightness as AirfreshLedBrightness, +) +from miio.integrations.airpurifier.zhimi.airpurifier import ( + LedBrightness as AirpurifierLedBrightness, +) +from miio.integrations.airpurifier.zhimi.airpurifier_miot import ( + LedBrightness as AirpurifierMiotLedBrightness, +) +from miio.integrations.humidifier.zhimi.airhumidifier import ( + LedBrightness as AirhumidifierLedBrightness, +) +from miio.integrations.humidifier.zhimi.airhumidifier_miot import ( + LedBrightness as AirhumidifierMiotLedBrightness, +) from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/requirements_all.txt b/requirements_all.txt index d43ee87da12..0606ec11933 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1930,7 +1930,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.11 +python-miio==0.5.12 # homeassistant.components.mpd python-mpd2==3.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee02d6f05a2..8148e114d6b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1298,7 +1298,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.xiaomi_miio -python-miio==0.5.11 +python-miio==0.5.12 # homeassistant.components.nest python-nest==4.2.0 From 503e88642e10281a463e681b278b2ea77d250f94 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 21:06:18 +0200 Subject: [PATCH 558/820] Update pyupgrade to 2.37.2 (#75456) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51429bdf94b..8e259c1d063 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.37.1 + rev: v2.37.2 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index b1a9a0a1a5b..c7f5d559c38 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.37.1 +pyupgrade==2.37.2 yamllint==1.27.1 From c29bd483735aa81c9ef572b0815c8a293f4f9046 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Jul 2022 00:22:51 +0000 Subject: [PATCH 559/820] [ci skip] Translation update --- .../components/google/translations/fr.json | 3 ++- .../components/google/translations/ru.json | 3 ++- .../homekit_controller/translations/fr.json | 4 ++-- .../components/lifx/translations/ca.json | 20 +++++++++++++++++++ .../components/lifx/translations/de.json | 20 +++++++++++++++++++ .../components/lifx/translations/et.json | 20 +++++++++++++++++++ .../components/lifx/translations/fr.json | 20 +++++++++++++++++++ .../components/lifx/translations/ru.json | 20 +++++++++++++++++++ 8 files changed, 106 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json index 7c5e4927787..389f769cdb9 100644 --- a/homeassistant/components/google/translations/fr.json +++ b/homeassistant/components/google/translations/fr.json @@ -11,7 +11,8 @@ "invalid_access_token": "Jeton d'acc\u00e8s non valide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "oauth_error": "Des donn\u00e9es de jeton non valides ont \u00e9t\u00e9 re\u00e7ues.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9" }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index bb47301624f..5d9f51fe14e 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -8,7 +8,8 @@ "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 01286ceb991..b46069de2b8 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -18,7 +18,7 @@ "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", "unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Annulez l'association sur tous les contr\u00f4leurs ou essayez de red\u00e9marrer l'appareil, puis continuez \u00e0 reprendre l'association.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Autoriser le jumelage avec des codes de configuration non s\u00e9curis\u00e9s.", "pairing_code": "Code d\u2019appairage" }, - "description": "Le contr\u00f4leur HomeKit communique avec {name} sur le r\u00e9seau local en utilisant une connexion crypt\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ou iCloud. Entrez votre code d'appariement HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.", + "description": "Le contr\u00f4leur HomeKit communique avec {name} ({category}) sur le r\u00e9seau local en utilisant une connexion chiffr\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ni iCloud. Saisissez votre code d'appairage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.", "title": "Couplage avec un appareil via le protocole accessoire HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/lifx/translations/ca.json b/homeassistant/components/lifx/translations/ca.json index 28c25cde70a..8d0efc4de8a 100644 --- a/homeassistant/components/lifx/translations/ca.json +++ b/homeassistant/components/lifx/translations/ca.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "no_devices_found": "No s'han trobat dispositius a la xarxa", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Vols configurar LIFX?" + }, + "discovery_confirm": { + "description": "Vols configurar {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositiu" + } + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Si deixes l'amfitri\u00f3 buit, s'utilitzar\u00e0 el descobriment per cercar dispositius." } } } diff --git a/homeassistant/components/lifx/translations/de.json b/homeassistant/components/lifx/translations/de.json index 0c619ea4062..82e37b39c8b 100644 --- a/homeassistant/components/lifx/translations/de.json +++ b/homeassistant/components/lifx/translations/de.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "M\u00f6chtest du LIFX einrichten?" + }, + "discovery_confirm": { + "description": "M\u00f6chtest du {label} ({host}) {serial} einrichten?" + }, + "pick_device": { + "data": { + "device": "Ger\u00e4t" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Wenn du den Host leer l\u00e4sst, wird die Erkennung verwendet, um Ger\u00e4te zu finden." } } } diff --git a/homeassistant/components/lifx/translations/et.json b/homeassistant/components/lifx/translations/et.json index ba833f79f8b..6d06cbb17ba 100644 --- a/homeassistant/components/lifx/translations/et.json +++ b/homeassistant/components/lifx/translations/et.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", "no_devices_found": "V\u00f5rgust ei leitud seadmeid", "single_instance_allowed": "Juba seadistatud, lubatud on ainult \u00fcks sidumine." }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Kas soovid seadistada LIFX-i?" + }, + "discovery_confirm": { + "description": "Kas seadistada {label} ( {host} ) {serial} ?" + }, + "pick_device": { + "data": { + "device": "Seade" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Kui j\u00e4tad hosti t\u00fchjaks kasutatakse seadmete leidmiseks avastamist." } } } diff --git a/homeassistant/components/lifx/translations/fr.json b/homeassistant/components/lifx/translations/fr.json index f8cc0a9dddd..c3f0561b085 100644 --- a/homeassistant/components/lifx/translations/fr.json +++ b/homeassistant/components/lifx/translations/fr.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Voulez-vous configurer LIFX?" + }, + "discovery_confirm": { + "description": "Voulez-vous configurer {label} ({host}) {serial}\u00a0?" + }, + "pick_device": { + "data": { + "device": "Appareil" + } + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } } diff --git a/homeassistant/components/lifx/translations/ru.json b/homeassistant/components/lifx/translations/ru.json index 0d50dec498b..9e9a9460e19 100644 --- a/homeassistant/components/lifx/translations/ru.json +++ b/homeassistant/components/lifx/translations/ru.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c LIFX?" + }, + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0415\u0441\u043b\u0438 \u043d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." } } } From 672883e19d1bdd2c26aa1f2f515a8fbc702bb518 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 02:50:37 +0200 Subject: [PATCH 560/820] Remove old type casting in esphome (#75475) --- homeassistant/components/esphome/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index bddb56a38d3..3d8455d3150 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -562,13 +562,11 @@ async def platform_async_setup_entry( """Update entities of this platform when entities are listed.""" old_infos = entry_data.info[component_key] new_infos: dict[int, EntityInfo] = {} - add_entities = [] + add_entities: list[_EntityT] = [] for info in infos: if not isinstance(info, info_type): # Filter out infos that don't belong to this platform. continue - # cast back to upper type, otherwise mypy gets confused - info = cast(EntityInfo, info) if info.key in old_infos: # Update existing entity From b04c3e9adc0e4eda2aa530eb116b6dc5f56d8822 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 02:54:46 +0200 Subject: [PATCH 561/820] Improve deprecation helper typing (#75453) --- .strict-typing | 1 + homeassistant/helpers/deprecation.py | 32 +++++++++++++++++++--------- mypy.ini | 3 +++ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index f133199a753..c184990884f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -16,6 +16,7 @@ homeassistant.auth.providers.* homeassistant.helpers.area_registry homeassistant.helpers.condition homeassistant.helpers.debounce +homeassistant.helpers.deprecation homeassistant.helpers.discovery homeassistant.helpers.entity homeassistant.helpers.entity_values diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 4bf57c1a4e1..8d961d7008b 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -5,12 +5,20 @@ from collections.abc import Callable import functools import inspect import logging -from typing import Any +from typing import Any, TypeVar + +from typing_extensions import ParamSpec from ..helpers.frame import MissingIntegrationFrame, get_integration_frame +_ObjectT = TypeVar("_ObjectT", bound=object) +_R = TypeVar("_R") +_P = ParamSpec("_P") -def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: + +def deprecated_substitute( + substitute_name: str, +) -> Callable[[Callable[[_ObjectT], Any]], Callable[[_ObjectT], Any]]: """Help migrate properties to new names. When a property is added to replace an older property, this decorator can @@ -19,10 +27,10 @@ def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: warning will be issued alerting the user of the impending change. """ - def decorator(func: Callable) -> Callable: + def decorator(func: Callable[[_ObjectT], Any]) -> Callable[[_ObjectT], Any]: """Decorate function as deprecated.""" - def func_wrapper(self: Callable) -> Any: + def func_wrapper(self: _ObjectT) -> Any: """Wrap for the original function.""" if hasattr(self, substitute_name): # If this platform is still using the old property, issue @@ -81,14 +89,16 @@ def get_deprecated( return config.get(new_name, default) -def deprecated_class(replacement: str) -> Any: +def deprecated_class( + replacement: str, +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: """Mark class as deprecated and provide a replacement class to be used instead.""" - def deprecated_decorator(cls: Any) -> Any: + def deprecated_decorator(cls: Callable[_P, _R]) -> Callable[_P, _R]: """Decorate class as deprecated.""" @functools.wraps(cls) - def deprecated_cls(*args: Any, **kwargs: Any) -> Any: + def deprecated_cls(*args: _P.args, **kwargs: _P.kwargs) -> _R: """Wrap for the original class.""" _print_deprecation_warning(cls, replacement, "class") return cls(*args, **kwargs) @@ -98,14 +108,16 @@ def deprecated_class(replacement: str) -> Any: return deprecated_decorator -def deprecated_function(replacement: str) -> Callable[..., Callable]: +def deprecated_function( + replacement: str, +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: """Mark function as deprecated and provide a replacement function to be used instead.""" - def deprecated_decorator(func: Callable) -> Callable: + def deprecated_decorator(func: Callable[_P, _R]) -> Callable[_P, _R]: """Decorate function as deprecated.""" @functools.wraps(func) - def deprecated_func(*args: Any, **kwargs: Any) -> Any: + def deprecated_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: """Wrap for the original function.""" _print_deprecation_warning(func, replacement, "function") return func(*args, **kwargs) diff --git a/mypy.ini b/mypy.ini index 3355680069e..6321ee04b98 100644 --- a/mypy.ini +++ b/mypy.ini @@ -60,6 +60,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.debounce] disallow_any_generics = true +[mypy-homeassistant.helpers.deprecation] +disallow_any_generics = true + [mypy-homeassistant.helpers.discovery] disallow_any_generics = true From d09fff595cf2ca3e7735309909f41501d83a75a0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 03:03:22 +0200 Subject: [PATCH 562/820] Rename existing TypeVars referencing Self type (#75473) --- homeassistant/backports/enum.py | 6 +++--- homeassistant/components/esphome/__init__.py | 6 +++--- homeassistant/components/zha/core/channels/__init__.py | 10 ++++++---- homeassistant/config_entries.py | 6 ++++-- homeassistant/helpers/restore_state.py | 4 ++-- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 9a96704a836..939d6e7669b 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -4,15 +4,15 @@ from __future__ import annotations from enum import Enum from typing import Any, TypeVar -_StrEnumT = TypeVar("_StrEnumT", bound="StrEnum") +_StrEnumSelfT = TypeVar("_StrEnumSelfT", bound="StrEnum") class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" def __new__( - cls: type[_StrEnumT], value: str, *args: Any, **kwargs: Any - ) -> _StrEnumT: + cls: type[_StrEnumSelfT], value: str, *args: Any, **kwargs: Any + ) -> _StrEnumSelfT: """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 3d8455d3150..5d7b0efc18d 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -58,7 +58,7 @@ from .entry_data import RuntimeEntryData DOMAIN = "esphome" CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) -_T = TypeVar("_T") +_DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") STORAGE_VERSION = 1 @@ -101,11 +101,11 @@ class DomainData: ) @classmethod - def get(cls: type[_T], hass: HomeAssistant) -> _T: + def get(cls: type[_DomainDataSelfT], hass: HomeAssistant) -> _DomainDataSelfT: """Get the global DomainData instance stored in hass.data.""" # Don't use setdefault - this is a hot code path if DOMAIN in hass.data: - return cast(_T, hass.data[DOMAIN]) + return cast(_DomainDataSelfT, hass.data[DOMAIN]) ret = hass.data[DOMAIN] = cls() return ret diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 9042856b456..849cdc29e47 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -36,8 +36,8 @@ if TYPE_CHECKING: from ...entity import ZhaEntity from ..device import ZHADevice -_ChannelsT = TypeVar("_ChannelsT", bound="Channels") -_ChannelPoolT = TypeVar("_ChannelPoolT", bound="ChannelPool") +_ChannelsSelfT = TypeVar("_ChannelsSelfT", bound="Channels") +_ChannelPoolSelfT = TypeVar("_ChannelPoolSelfT", bound="ChannelPool") _ChannelsDictType = dict[str, base.ZigbeeChannel] @@ -104,7 +104,7 @@ class Channels: } @classmethod - def new(cls: type[_ChannelsT], zha_device: ZHADevice) -> _ChannelsT: + def new(cls: type[_ChannelsSelfT], zha_device: ZHADevice) -> _ChannelsSelfT: """Create new instance.""" channels = cls(zha_device) for ep_id in sorted(zha_device.device.endpoints): @@ -272,7 +272,9 @@ class ChannelPool: ) @classmethod - def new(cls: type[_ChannelPoolT], channels: Channels, ep_id: int) -> _ChannelPoolT: + def new( + cls: type[_ChannelPoolSelfT], channels: Channels, ep_id: int + ) -> _ChannelPoolSelfT: """Create new channels for an endpoint.""" pool = cls(channels, ep_id) pool.add_all_channels() diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 232d3e5cbf1..b5d5804f100 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -73,7 +73,7 @@ PATH_CONFIG = ".config_entries.json" SAVE_DELAY = 1 -_T = TypeVar("_T", bound="ConfigEntryState") +_ConfigEntryStateSelfT = TypeVar("_ConfigEntryStateSelfT", bound="ConfigEntryState") _R = TypeVar("_R") @@ -97,7 +97,9 @@ class ConfigEntryState(Enum): _recoverable: bool - def __new__(cls: type[_T], value: str, recoverable: bool) -> _T: + def __new__( + cls: type[_ConfigEntryStateSelfT], value: str, recoverable: bool + ) -> _ConfigEntryStateSelfT: """Create new ConfigEntryState.""" obj = object.__new__(cls) obj._value_ = value diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 4f2d1dd0503..314daecde48 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -32,7 +32,7 @@ STATE_DUMP_INTERVAL = timedelta(minutes=15) # How long should a saved state be preserved if the entity no longer exists STATE_EXPIRATION = timedelta(days=7) -_StoredStateT = TypeVar("_StoredStateT", bound="StoredState") +_StoredStateSelfT = TypeVar("_StoredStateSelfT", bound="StoredState") class ExtraStoredData: @@ -82,7 +82,7 @@ class StoredState: return result @classmethod - def from_dict(cls: type[_StoredStateT], json_dict: dict) -> _StoredStateT: + def from_dict(cls: type[_StoredStateSelfT], json_dict: dict) -> _StoredStateSelfT: """Initialize a stored state from a dict.""" extra_data_dict = json_dict.get("extra_data") extra_data = RestoredExtraData(extra_data_dict) if extra_data_dict else None From 07b4d48e7c1cadb84a70026bf20d46024387b0d1 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 19 Jul 2022 21:23:14 -0400 Subject: [PATCH 563/820] Disable Aladdin Connect battery_level by default (#75441) * Disable battery_level by default * Removed async_setup_compnent, renamed constant. --- .../components/aladdin_connect/sensor.py | 1 + tests/components/aladdin_connect/test_init.py | 12 +-- .../components/aladdin_connect/test_sensor.py | 85 +++++++++++++++++++ 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 tests/components/aladdin_connect/test_sensor.py diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index d9783d0f61d..68631c57fc8 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -42,6 +42,7 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = ( key="battery_level", name="Battery level", device_class=SensorDeviceClass.BATTERY, + entity_registry_enabled_default=False, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value_fn=AladdinConnectClient.get_battery_status, diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index 4c422ae29ba..4cbb2333cc5 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -9,14 +9,14 @@ from homeassistant.core import HomeAssistant from tests.common import AsyncMock, MockConfigEntry -YAML_CONFIG = {"username": "test-user", "password": "test-password"} +CONFIG = {"username": "test-user", "password": "test-password"} async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: """Test component setup Get Doors Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -38,7 +38,7 @@ async def test_setup_login_error( """Test component setup Login Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -57,7 +57,7 @@ async def test_setup_connection_error( """Test component setup Login Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -74,7 +74,7 @@ async def test_setup_component_no_error(hass: HomeAssistant) -> None: """Test component setup No Error.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -116,7 +116,7 @@ async def test_load_and_unload( """Test loading and unloading Aladdin Connect entry.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) diff --git a/tests/components/aladdin_connect/test_sensor.py b/tests/components/aladdin_connect/test_sensor.py new file mode 100644 index 00000000000..3702bcd9efa --- /dev/null +++ b/tests/components/aladdin_connect/test_sensor.py @@ -0,0 +1,85 @@ +"""Test the Aladdin Connect Sensors.""" +from datetime import timedelta +from unittest.mock import MagicMock, patch + +from homeassistant.components.aladdin_connect.const import DOMAIN +from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, async_fire_time_changed + +CONFIG = {"username": "test-user", "password": "test-password"} +RELOAD_AFTER_UPDATE_DELAY = timedelta(seconds=31) + + +async def test_sensors( + hass: HomeAssistant, + mock_aladdinconnect_api: MagicMock, +) -> None: + """Test Sensors for AladdinConnect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + + await hass.async_block_till_done() + + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entry = registry.async_get("sensor.home_battery_level") + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get("sensor.home_battery_level") + assert state is None + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.home_battery_level") + assert state + + entry = registry.async_get("sensor.home_wi_fi_rssi") + await hass.async_block_till_done() + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state is None + + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state From 3193ea3359573f35e6929015f39c15b9f7170e59 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 04:00:14 +0200 Subject: [PATCH 564/820] Fix type narrowing in energy integration (#75462) --- homeassistant/components/energy/data.py | 2 +- homeassistant/components/energy/validate.py | 23 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index e8c62da0c3c..3b3952136be 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -54,7 +54,7 @@ class FlowToGridSourceType(TypedDict): stat_compensation: str | None # Used to generate costs if stat_compensation is set to None - entity_energy_from: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_from + entity_energy_to: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_to entity_energy_price: str | None # entity_id of an entity providing price ($/kWh) number_energy_price: float | None # Price for energy ($/kWh) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 3baae348770..02549ddfe96 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -263,10 +263,10 @@ def _async_validate_auto_generated_cost_entity( async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: """Validate the energy configuration.""" - manager = await data.async_get_manager(hass) + manager: data.EnergyManager = await data.async_get_manager(hass) statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]] = {} validate_calls = [] - wanted_statistics_metadata = set() + wanted_statistics_metadata: set[str] = set() result = EnergyPreferencesValidation() @@ -279,6 +279,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: result.energy_sources.append(source_result) if source["type"] == "grid": + flow: data.FlowFromGridSourceType | data.FlowToGridSourceType for flow in source["flow_from"]: wanted_statistics_metadata.add(flow["stat_energy_from"]) validate_calls.append( @@ -294,14 +295,14 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("stat_cost") is not None: - wanted_statistics_metadata.add(flow["stat_cost"]) + if (stat_cost := flow.get("stat_cost")) is not None: + wanted_statistics_metadata.add(stat_cost) validate_calls.append( functools.partial( _async_validate_cost_stat, hass, statistics_metadata, - flow["stat_cost"], + stat_cost, source_result, ) ) @@ -345,14 +346,14 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("stat_compensation") is not None: - wanted_statistics_metadata.add(flow["stat_compensation"]) + if (stat_compensation := flow.get("stat_compensation")) is not None: + wanted_statistics_metadata.add(stat_compensation) validate_calls.append( functools.partial( _async_validate_cost_stat, hass, statistics_metadata, - flow["stat_compensation"], + stat_compensation, source_result, ) ) @@ -396,14 +397,14 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if source.get("stat_cost") is not None: - wanted_statistics_metadata.add(source["stat_cost"]) + if (stat_cost := source.get("stat_cost")) is not None: + wanted_statistics_metadata.add(stat_cost) validate_calls.append( functools.partial( _async_validate_cost_stat, hass, statistics_metadata, - source["stat_cost"], + stat_cost, source_result, ) ) From 1a1eeb2274903cb18f4fb99a4ea3af94f4a04059 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 04:02:03 +0200 Subject: [PATCH 565/820] Allow for subclass typing with StatisticsBase (#75476) --- homeassistant/components/recorder/db_schema.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 36487353e25..4777eeb500e 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta import logging -from typing import Any, cast +from typing import Any, TypeVar, cast import ciso8601 from fnvhash import fnv1a_32 @@ -55,6 +55,8 @@ Base = declarative_base() SCHEMA_VERSION = 29 +_StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") + _LOGGER = logging.getLogger(__name__) TABLE_EVENTS = "events" @@ -443,7 +445,9 @@ class StatisticsBase: sum = Column(DOUBLE_TYPE) @classmethod - def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: + def from_stats( + cls: type[_StatisticsBaseSelfT], metadata_id: int, stats: StatisticData + ) -> _StatisticsBaseSelfT: """Create object from a statistics.""" return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, From 1626c53c13866cc1fb0b19db2175dcde6af5f097 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 04:11:46 +0200 Subject: [PATCH 566/820] Improve dispatcher helper typing (#75455) * Improve dispatcher helper typing * Code review --- .strict-typing | 1 + homeassistant/helpers/dispatcher.py | 14 +++++++++----- mypy.ini | 3 +++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.strict-typing b/.strict-typing index c184990884f..9757a5f6c03 100644 --- a/.strict-typing +++ b/.strict-typing @@ -18,6 +18,7 @@ homeassistant.helpers.condition homeassistant.helpers.debounce homeassistant.helpers.deprecation homeassistant.helpers.discovery +homeassistant.helpers.dispatcher homeassistant.helpers.entity homeassistant.helpers.entity_values homeassistant.helpers.event diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 07a3e3b3b28..c7ad4fb1adf 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -1,7 +1,7 @@ """Helpers for Home Assistant dispatcher & internal component/platform.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine import logging from typing import Any @@ -62,7 +62,9 @@ def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args) -def _generate_job(signal: str, target: Callable[..., Any]) -> HassJob: +def _generate_job( + signal: str, target: Callable[..., Any] +) -> HassJob[..., None | Coroutine[Any, Any, None]]: """Generate a HassJob for a signal and target.""" return HassJob( catch_log_exception( @@ -84,16 +86,18 @@ def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: This method must be run in the event loop. """ - target_list = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) + target_list: dict[ + Callable[..., Any], HassJob[..., None | Coroutine[Any, Any, None]] | None + ] = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) - run: list[HassJob] = [] + run: list[HassJob[..., None | Coroutine[Any, Any, None]]] = [] for target, job in target_list.items(): if job is None: job = _generate_job(signal, target) target_list[target] = job # Run the jobs all at the end - # to ensure no jobs add more disptachers + # to ensure no jobs add more dispatchers # which can result in the target_list # changing size during iteration run.append(job) diff --git a/mypy.ini b/mypy.ini index 6321ee04b98..d7f3db26d55 100644 --- a/mypy.ini +++ b/mypy.ini @@ -66,6 +66,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.discovery] disallow_any_generics = true +[mypy-homeassistant.helpers.dispatcher] +disallow_any_generics = true + [mypy-homeassistant.helpers.entity] disallow_any_generics = true From 0f81d1d14ad92dfaa012dee173e664d4cae4fd6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Jul 2022 21:14:31 -0500 Subject: [PATCH 567/820] Drop RSSI update workaround from bluetooth on linux (#75467) It turns out we do not need these are we can check the discovered device list to see if bluez is still seeing the device --- homeassistant/components/bluetooth/__init__.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8a1897c5ec2..33b2da7523e 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from enum import Enum import fnmatch import logging -import platform from typing import Final, TypedDict, Union from bleak import BleakError @@ -217,20 +216,6 @@ def _ble_device_matches( return True -@hass_callback -def async_enable_rssi_updates() -> None: - """Bleak filters out RSSI updates by default on linux only.""" - # We want RSSI updates - if platform.system() == "Linux": - from bleak.backends.bluezdbus import ( # pylint: disable=import-outside-toplevel - scanner, - ) - - scanner._ADVERTISING_DATA_PROPERTIES.add( # pylint: disable=protected-access - "RSSI" - ) - - class BluetoothManager: """Manage Bluetooth.""" @@ -265,7 +250,6 @@ class BluetoothManager: ex, ) return - async_enable_rssi_updates() install_multiple_bleak_catcher(self.scanner) # We have to start it right away as some integrations might # need it straight away. From 8a48d549513539ffc9d3928b4b15c124f5ce6e5f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 05:45:57 +0200 Subject: [PATCH 568/820] Improve entity_platform helper typing (#75464) * Improve entity_platform helper typing * Add protocol class * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + homeassistant/helpers/entity_platform.py | 55 ++++++++++++++++++------ mypy.ini | 3 ++ 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/.strict-typing b/.strict-typing index 9757a5f6c03..46d85aa1ec2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -20,6 +20,7 @@ homeassistant.helpers.deprecation homeassistant.helpers.discovery homeassistant.helpers.dispatcher homeassistant.helpers.entity +homeassistant.helpers.entity_platform homeassistant.helpers.entity_values homeassistant.helpers.event homeassistant.helpers.reload diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 2049565e859..c5c61cc1b0d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -2,11 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import Awaitable, Callable, Coroutine, Iterable from contextvars import ContextVar from datetime import datetime, timedelta from logging import Logger, getLogger -from types import ModuleType from typing import TYPE_CHECKING, Any, Protocol from urllib.parse import urlparse @@ -71,6 +70,36 @@ class AddEntitiesCallback(Protocol): """Define add_entities type.""" +class EntityPlatformModule(Protocol): + """Protocol type for entity platform modules.""" + + async def async_setup_platform( + self, + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up an integration platform async.""" + + def setup_platform( + self, + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up an integration platform.""" + + async def async_setup_entry( + self, + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up an integration platform from a config entry.""" + + class EntityPlatform: """Manage the entities for a single platform.""" @@ -81,7 +110,7 @@ class EntityPlatform: logger: Logger, domain: str, platform_name: str, - platform: ModuleType | None, + platform: EntityPlatformModule | None, scan_interval: timedelta, entity_namespace: str | None, ) -> None: @@ -95,7 +124,7 @@ class EntityPlatform: self.entity_namespace = entity_namespace self.config_entry: config_entries.ConfigEntry | None = None self.entities: dict[str, Entity] = {} - self._tasks: list[asyncio.Future] = [] + self._tasks: list[asyncio.Task[None]] = [] # Stop tracking tasks after setup is completed self._setup_complete = False # Method to cancel the state change listener @@ -169,10 +198,12 @@ class EntityPlatform: return @callback - def async_create_setup_task() -> Coroutine: + def async_create_setup_task() -> Coroutine[ + Any, Any, None + ] | asyncio.Future[None]: """Get task to set up platform.""" if getattr(platform, "async_setup_platform", None): - return platform.async_setup_platform( # type: ignore[no-any-return,union-attr] + return platform.async_setup_platform( # type: ignore[union-attr] hass, platform_config, self._async_schedule_add_entities, @@ -181,7 +212,7 @@ class EntityPlatform: # This should not be replaced with hass.async_add_job because # we don't want to track this task in case it blocks startup. - return hass.loop.run_in_executor( # type: ignore[return-value] + return hass.loop.run_in_executor( None, platform.setup_platform, # type: ignore[union-attr] hass, @@ -211,18 +242,18 @@ class EntityPlatform: platform = self.platform @callback - def async_create_setup_task() -> Coroutine: + def async_create_setup_task() -> Coroutine[Any, Any, None]: """Get task to set up platform.""" config_entries.current_entry.set(config_entry) - return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] + return platform.async_setup_entry( # type: ignore[union-attr] self.hass, config_entry, self._async_schedule_add_entities_for_entry ) return await self._async_setup_platform(async_create_setup_task) async def _async_setup_platform( - self, async_create_setup_task: Callable[[], Coroutine], tries: int = 0 + self, async_create_setup_task: Callable[[], Awaitable[None]], tries: int = 0 ) -> bool: """Set up a platform via config file or config entry. @@ -701,7 +732,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: dict | vol.Schema, + schema: dict[str, Any] | vol.Schema, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, ) -> None: @@ -753,7 +784,7 @@ class EntityPlatform: return async with self._process_updates: - tasks = [] + tasks: list[Coroutine[Any, Any, None]] = [] for entity in self.entities.values(): if not entity.should_poll: continue diff --git a/mypy.ini b/mypy.ini index d7f3db26d55..04b76dfea0f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -72,6 +72,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.entity] disallow_any_generics = true +[mypy-homeassistant.helpers.entity_platform] +disallow_any_generics = true + [mypy-homeassistant.helpers.entity_values] disallow_any_generics = true From 51ed9ee59d21e2a065a4b1799075258a950e1e4a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 05:46:18 +0200 Subject: [PATCH 569/820] Fix bluetooth service_info typing (#75477) * Fix bluetooth service_info typing * Remove additional type ignores * Remove pylint disable --- homeassistant/components/bluetooth/__init__.py | 6 +++--- .../components/bluetooth_tracker/device_tracker.py | 6 +++--- homeassistant/helpers/service_info/__init__.py | 1 + homeassistant/helpers/service_info/bluetooth.py | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 homeassistant/helpers/service_info/__init__.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 33b2da7523e..5edc0053203 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -43,7 +43,7 @@ SOURCE_LOCAL: Final = "local" @dataclass -class BluetoothServiceInfoBleak(BluetoothServiceInfo): # type: ignore[misc] +class BluetoothServiceInfoBleak(BluetoothServiceInfo): """BluetoothServiceInfo with bleak data. Integrations may need BLEDevice and AdvertisementData @@ -58,7 +58,7 @@ class BluetoothServiceInfoBleak(BluetoothServiceInfo): # type: ignore[misc] @classmethod def from_advertisement( cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str - ) -> BluetoothServiceInfo: + ) -> BluetoothServiceInfoBleak: """Create a BluetoothServiceInfoBleak from an advertisement.""" return cls( name=advertisement_data.local_name or device.name or device.address, @@ -377,7 +377,7 @@ class BluetoothManager: ) @hass_callback - def async_discovered_service_info(self) -> list[BluetoothServiceInfo]: + def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: """Return if the address is present.""" if models.HA_BLEAK_SCANNER: discovered = models.HA_BLEAK_SCANNER.discovered_devices diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 5cf173d356e..b62333c0489 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -61,7 +61,7 @@ def is_bluetooth_device(device: Device) -> bool: def discover_devices(device_id: int) -> list[tuple[str, str]]: """Discover Bluetooth devices.""" try: - result = bluetooth.discover_devices( # type: ignore[attr-defined] + result = bluetooth.discover_devices( duration=8, lookup_names=True, flush_cache=True, @@ -124,7 +124,7 @@ async def get_tracking_devices(hass: HomeAssistant) -> tuple[set[str], set[str]] def lookup_name(mac: str) -> str | None: """Lookup a Bluetooth device name.""" _LOGGER.debug("Scanning %s", mac) - return bluetooth.lookup_name(mac, timeout=5) # type: ignore[attr-defined,no-any-return] + return bluetooth.lookup_name(mac, timeout=5) # type: ignore[no-any-return] async def async_setup_scanner( @@ -180,7 +180,7 @@ async def async_setup_scanner( if tasks: await asyncio.wait(tasks) - except bluetooth.BluetoothError: # type: ignore[attr-defined] + except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") async def update_bluetooth(now: datetime | None = None) -> None: diff --git a/homeassistant/helpers/service_info/__init__.py b/homeassistant/helpers/service_info/__init__.py new file mode 100644 index 00000000000..b907fd9bbd1 --- /dev/null +++ b/homeassistant/helpers/service_info/__init__.py @@ -0,0 +1 @@ +"""Service info helpers.""" diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index d4d74a45be4..968d1dde95f 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -1,4 +1,4 @@ """The bluetooth integration service info.""" -from home_assistant_bluetooth import ( # pylint: disable=unused-import # noqa: F401 - BluetoothServiceInfo, -) +from home_assistant_bluetooth import BluetoothServiceInfo + +__all__ = ["BluetoothServiceInfo"] From d989e4373d576c403790c9a7e5eb7a29d08e3c47 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 20 Jul 2022 12:08:11 +0800 Subject: [PATCH 570/820] Remove websocket_api send_big_result (#75452) --- homeassistant/components/camera/__init__.py | 34 +--------- .../components/lovelace/websocket.py | 5 +- .../components/media_player/__init__.py | 46 ------------- .../components/websocket_api/connection.py | 5 -- .../components/androidtv/test_media_player.py | 65 +++++++------------ tests/components/camera/test_init.py | 19 ------ tests/components/media_player/test_init.py | 34 ---------- .../websocket_api/test_connection.py | 20 ------ 8 files changed, 27 insertions(+), 201 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 35fa6fef1d6..dcee6c6c5ed 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -import base64 import collections from collections.abc import Awaitable, Callable, Iterable from contextlib import suppress @@ -362,12 +361,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(CameraImageView(component)) hass.http.register_view(CameraMjpegStream(component)) - websocket_api.async_register_command( - hass, - WS_TYPE_CAMERA_THUMBNAIL, - websocket_camera_thumbnail, - SCHEMA_WS_CAMERA_THUMBNAIL, - ) + websocket_api.async_register_command(hass, ws_camera_stream) websocket_api.async_register_command(hass, ws_camera_web_rtc_offer) websocket_api.async_register_command(hass, websocket_get_prefs) @@ -793,32 +787,6 @@ class CameraMjpegStream(CameraView): raise web.HTTPBadRequest() from err -@websocket_api.async_response -async def websocket_camera_thumbnail( - hass: HomeAssistant, connection: ActiveConnection, msg: dict -) -> None: - """Handle get camera thumbnail websocket command. - - Async friendly. - """ - _LOGGER.warning("The websocket command 'camera_thumbnail' has been deprecated") - try: - image = await async_get_image(hass, msg["entity_id"]) - connection.send_big_result( - msg["id"], - { - "content_type": image.content_type, - "content": base64.b64encode(image.content).decode("utf-8"), - }, - ) - except HomeAssistantError: - connection.send_message( - websocket_api.error_message( - msg["id"], "image_fetch_failed", "Unable to fetch image" - ) - ) - - @websocket_api.websocket_command( { vol.Required("type"): "camera/stream", diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index 7e04201291d..66ec7f22b09 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -37,10 +37,7 @@ def _handle_errors(func): connection.send_error(msg["id"], *error) return - if msg is not None: - connection.send_big_result(msg["id"], result) - else: - connection.send_result(msg["id"], result) + connection.send_result(msg["id"], result) return send_with_error_handling diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 5e5347e6806..9ca9613278d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -import base64 import collections from collections.abc import Callable from contextlib import suppress @@ -27,7 +26,6 @@ from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.websocket_api.const import ( - ERR_NOT_FOUND, ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) @@ -254,7 +252,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) - websocket_api.async_register_command(hass, websocket_handle_thumbnail) websocket_api.async_register_command(hass, websocket_browse_media) hass.http.register_view(MediaPlayerImageView(component)) @@ -1130,49 +1127,6 @@ class MediaPlayerImageView(HomeAssistantView): return web.Response(body=data, content_type=content_type, headers=headers) -@websocket_api.websocket_command( - { - vol.Required("type"): "media_player_thumbnail", - vol.Required("entity_id"): cv.entity_id, - } -) -@websocket_api.async_response -async def websocket_handle_thumbnail(hass, connection, msg): - """Handle get media player cover command. - - Async friendly. - """ - component = hass.data[DOMAIN] - - if (player := component.get_entity(msg["entity_id"])) is None: - connection.send_message( - websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") - ) - return - - _LOGGER.warning( - "The websocket command media_player_thumbnail is deprecated. Use /api/media_player_proxy instead" - ) - - data, content_type = await player.async_get_media_image() - - if data is None: - connection.send_message( - websocket_api.error_message( - msg["id"], "thumbnail_fetch_failed", "Failed to fetch thumbnail" - ) - ) - return - - connection.send_big_result( - msg["id"], - { - "content_type": content_type, - "content": base64.b64encode(data).decode("utf-8"), - }, - ) - - @websocket_api.websocket_command( { vol.Required("type"): "media_player/browse_media", diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 1c26d958969..87c52288bcc 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.auth.models import RefreshToken, User from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized -from homeassistant.helpers.json import JSON_DUMP from . import const, messages @@ -54,10 +53,6 @@ class ActiveConnection: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) - def send_big_result(self, msg_id: int, result: Any) -> None: - """Send a result message that would be expensive to JSON serialize.""" - self.send_message(JSON_DUMP(messages.result_message(msg_id, result))) - @callback def send_error(self, msg_id: int, code: str, message: str) -> None: """Send a error message.""" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 73f8de55cc9..f5487c78425 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,5 +1,4 @@ """The tests for the androidtv platform.""" -import base64 import logging from unittest.mock import Mock, patch @@ -52,7 +51,6 @@ from homeassistant.components.media_player import ( SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, ) -from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_COMMAND, @@ -851,10 +849,10 @@ async def test_androidtv_volume_set(hass): patch_set_volume_level.assert_called_with(0.5) -async def test_get_image(hass, hass_ws_client): +async def test_get_image_http(hass, hass_client_no_auth): """Test taking a screen capture. - This is based on `test_get_image` in tests/components/media_player/test_init.py. + This is based on `test_get_image_http` in tests/components/media_player/test_init.py. """ patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) @@ -868,44 +866,36 @@ async def test_get_image(hass, hass_ws_client): with patchers.patch_shell("11")[patch_key]: await async_update_entity(hass, entity_id) - client = await hass_ws_client(hass) + media_player_name = "media_player." + slugify( + CONFIG_ANDROIDTV_DEFAULT[TEST_ENTITY_NAME] + ) + state = hass.states.get(media_player_name) + assert "entity_picture_local" not in state.attributes + + client = await hass_client_no_auth() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", return_value=b"image" ): - await client.send_json( - {"id": 5, "type": "media_player_thumbnail", "entity_id": entity_id} - ) + resp = await client.get(state.attributes["entity_picture"]) + content = await resp.read() - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"]["content_type"] == "image/png" - assert msg["result"]["content"] == base64.b64encode(b"image").decode("utf-8") + assert content == b"image" with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", side_effect=ConnectionResetError, ): - await client.send_json( - {"id": 6, "type": "media_player_thumbnail", "entity_id": entity_id} - ) + resp = await client.get(state.attributes["entity_picture"]) - msg = await client.receive_json() - - # The device is unavailable, but getting the media image did not cause an exception - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_UNAVAILABLE + # The device is unavailable, but getting the media image did not cause an exception + state = hass.states.get(media_player_name) + assert state is not None + assert state.state == STATE_UNAVAILABLE -async def test_get_image_disabled(hass, hass_ws_client): - """Test taking a screen capture with screencap option disabled. - - This is based on `test_get_image` in tests/components/media_player/test_init.py. - """ +async def test_get_image_disabled(hass): + """Test that the screencap option can disable entity_picture.""" patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( @@ -921,17 +911,12 @@ async def test_get_image_disabled(hass, hass_ws_client): with patchers.patch_shell("11")[patch_key]: await async_update_entity(hass, entity_id) - client = await hass_ws_client(hass) - - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", return_value=b"image" - ) as screen_cap: - await client.send_json( - {"id": 5, "type": "media_player_thumbnail", "entity_id": entity_id} - ) - - await client.receive_json() - assert not screen_cap.called + media_player_name = "media_player." + slugify( + CONFIG_ANDROIDTV_DEFAULT[TEST_ENTITY_NAME] + ) + state = hass.states.get(media_player_name) + assert "entity_picture_local" not in state.attributes + assert "entity_picture" not in state.attributes async def _test_service( diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index ba13bbd6c52..f800a4ac2bd 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,6 +1,5 @@ """The tests for the camera component.""" import asyncio -import base64 from http import HTTPStatus import io from unittest.mock import AsyncMock, Mock, PropertyMock, mock_open, patch @@ -235,24 +234,6 @@ async def test_snapshot_service(hass, mock_camera): assert mock_write.mock_calls[0][1][0] == b"Test" -async def test_websocket_camera_thumbnail(hass, hass_ws_client, mock_camera): - """Test camera_thumbnail websocket command.""" - await async_setup_component(hass, "camera", {}) - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "camera_thumbnail", "entity_id": "camera.demo_camera"} - ) - - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"]["content_type"] == "image/jpg" - assert msg["result"]["content"] == base64.b64encode(b"Test").decode("utf-8") - - async def test_websocket_stream_no_source( hass, hass_ws_client, mock_camera, mock_stream ): diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index eceb7e9ec4f..f0666a11545 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,6 +1,5 @@ """Test the base functions of the media player.""" import asyncio -import base64 from http import HTTPStatus from unittest.mock import patch @@ -14,39 +13,6 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.setup import async_setup_component -async def test_get_image(hass, hass_ws_client, caplog): - """Test get image via WS command.""" - await async_setup_component( - hass, "media_player", {"media_player": {"platform": "demo"}} - ) - await hass.async_block_till_done() - - client = await hass_ws_client(hass) - - with patch( - "homeassistant.components.media_player.MediaPlayerEntity." - "async_get_media_image", - return_value=(b"image", "image/jpeg"), - ): - await client.send_json( - { - "id": 5, - "type": "media_player_thumbnail", - "entity_id": "media_player.bedroom", - } - ) - - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"]["content_type"] == "image/jpeg" - assert msg["result"]["content"] == base64.b64encode(b"image").decode("utf-8") - - assert "media_player_thumbnail is deprecated" in caplog.text - - async def test_get_image_http(hass, hass_client_no_auth): """Test get image via http command.""" await async_setup_component( diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index da31f0ee8a3..fd9af99c1a4 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -7,30 +7,10 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.components import websocket_api -from homeassistant.components.websocket_api import const from tests.common import MockUser -async def test_send_big_result(hass, websocket_client): - """Test sending big results over the WS.""" - - @websocket_api.websocket_command({"type": "big_result"}) - @websocket_api.async_response - def send_big_result(hass, connection, msg): - connection.send_big_result(msg["id"], {"big": "result"}) - - websocket_api.async_register_command(hass, send_big_result) - - await websocket_client.send_json({"id": 5, "type": "big_result"}) - - msg = await websocket_client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert msg["success"] - assert msg["result"] == {"big": "result"} - - async def test_exception_handling(): """Test handling of exceptions.""" send_messages = [] From 8c7e329754a2ef2d07810ccd30b8499d0d4fe0f7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 20 Jul 2022 03:15:36 -0400 Subject: [PATCH 571/820] Bump pytomorrowio to 0.3.4 (#75478) --- homeassistant/components/tomorrowio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json index 5447b90d1ce..0823b5ac185 100644 --- a/homeassistant/components/tomorrowio/manifest.json +++ b/homeassistant/components/tomorrowio/manifest.json @@ -3,7 +3,7 @@ "name": "Tomorrow.io", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tomorrowio", - "requirements": ["pytomorrowio==0.3.3"], + "requirements": ["pytomorrowio==0.3.4"], "codeowners": ["@raman325", "@lymanepp"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0606ec11933..4fb9282d0ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1978,7 +1978,7 @@ pythonegardia==1.0.40 pytile==2022.02.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.3 +pytomorrowio==0.3.4 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8148e114d6b..6ea366315f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1325,7 +1325,7 @@ python_awair==0.2.3 pytile==2022.02.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.3 +pytomorrowio==0.3.4 # homeassistant.components.traccar pytraccar==0.10.0 From 2b752355d63f6fb906ab47db80865d84797a99bb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Jul 2022 03:15:30 -0600 Subject: [PATCH 572/820] Modify Tile to store a single dataclass in `hass.data` (#75459) --- homeassistant/components/tile/__init__.py | 16 +++++++++++----- homeassistant/components/tile/const.py | 3 --- homeassistant/components/tile/device_tracker.py | 13 ++++++------- homeassistant/components/tile/diagnostics.py | 9 ++++----- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index cca0706954f..38f52a067fa 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -1,6 +1,7 @@ """The Tile component.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from functools import partial @@ -17,7 +18,7 @@ from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_e from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.async_ import gather_with_concurrency -from .const import DATA_COORDINATOR, DATA_TILE, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER PLATFORMS = [Platform.DEVICE_TRACKER] DEVICE_TYPES = ["PHONE", "TILE"] @@ -28,6 +29,14 @@ DEFAULT_UPDATE_INTERVAL = timedelta(minutes=2) CONF_SHOW_INACTIVE = "show_inactive" +@dataclass +class TileData: + """Define an object to be stored in `hass.data`.""" + + coordinators: dict[str, DataUpdateCoordinator] + tiles: dict[str, Tile] + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tile as config entry.""" @@ -100,10 +109,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_COORDINATOR: coordinators, - DATA_TILE: tiles, - } + hass.data[DOMAIN][entry.entry_id] = TileData(coordinators=coordinators, tiles=tiles) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/tile/const.py b/homeassistant/components/tile/const.py index 0f6f0dabb5c..eed3eb698ef 100644 --- a/homeassistant/components/tile/const.py +++ b/homeassistant/components/tile/const.py @@ -3,7 +3,4 @@ import logging DOMAIN = "tile" -DATA_COORDINATOR = "coordinator" -DATA_TILE = "tile" - LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index df5f035a849..61e9a1bdcd9 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -18,7 +18,8 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DATA_COORDINATOR, DATA_TILE, DOMAIN +from . import TileData +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -38,14 +39,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Tile device trackers.""" + data: TileData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( [ - TileDeviceTracker( - entry, - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][tile_uuid], - tile, - ) - for tile_uuid, tile in hass.data[DOMAIN][entry.entry_id][DATA_TILE].items() + TileDeviceTracker(entry, data.coordinators[tile_uuid], tile) + for tile_uuid, tile in data.tiles.items() ] ) diff --git a/homeassistant/components/tile/diagnostics.py b/homeassistant/components/tile/diagnostics.py index 7e85ef25047..8654bf44680 100644 --- a/homeassistant/components/tile/diagnostics.py +++ b/homeassistant/components/tile/diagnostics.py @@ -3,14 +3,13 @@ from __future__ import annotations from typing import Any -from pytile.tile import Tile - from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from .const import DATA_TILE, DOMAIN +from . import TileData +from .const import DOMAIN CONF_ALTITUDE = "altitude" CONF_UUID = "uuid" @@ -27,8 +26,8 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - tiles: dict[str, Tile] = hass.data[DOMAIN][entry.entry_id][DATA_TILE] + data: TileData = hass.data[DOMAIN][entry.entry_id] return async_redact_data( - {"tiles": [tile.as_dict() for tile in tiles.values()]}, TO_REDACT + {"tiles": [tile.as_dict() for tile in data.tiles.values()]}, TO_REDACT ) From 3d31e626831f2df7c039df54bd1710d0d2c01fce Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Jul 2022 03:18:26 -0600 Subject: [PATCH 573/820] Modify Ridwell to store a single dataclass in `hass.data` (#75457) --- homeassistant/components/ridwell/__init__.py | 24 ++++++++++--------- homeassistant/components/ridwell/const.py | 3 --- .../components/ridwell/diagnostics.py | 10 ++++---- homeassistant/components/ridwell/sensor.py | 11 ++++----- homeassistant/components/ridwell/switch.py | 11 ++++----- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 5f3656a8b5a..5a9c19ed36f 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -21,19 +22,21 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import ( - DATA_ACCOUNT, - DATA_COORDINATOR, - DOMAIN, - LOGGER, - SENSOR_TYPE_NEXT_PICKUP, -) +from .const import DOMAIN, LOGGER, SENSOR_TYPE_NEXT_PICKUP DEFAULT_UPDATE_INTERVAL = timedelta(hours=1) PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH] +@dataclass +class RidwellData: + """Define an object to be stored in `hass.data`.""" + + accounts: dict[str, RidwellAccount] + coordinator: DataUpdateCoordinator + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ridwell from a config entry.""" session = aiohttp_client.async_get_clientsession(hass) @@ -77,10 +80,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_ACCOUNT: accounts, - DATA_COORDINATOR: coordinator, - } + hass.data[DOMAIN][entry.entry_id] = RidwellData( + accounts=accounts, coordinator=coordinator + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/ridwell/const.py b/homeassistant/components/ridwell/const.py index bc1e78e2d93..69c5ead5277 100644 --- a/homeassistant/components/ridwell/const.py +++ b/homeassistant/components/ridwell/const.py @@ -5,7 +5,4 @@ DOMAIN = "ridwell" LOGGER = logging.getLogger(__package__) -DATA_ACCOUNT = "account" -DATA_COORDINATOR = "coordinator" - SENSOR_TYPE_NEXT_PICKUP = "next_pickup" diff --git a/homeassistant/components/ridwell/diagnostics.py b/homeassistant/components/ridwell/diagnostics.py index fce89a639ad..3f29165842f 100644 --- a/homeassistant/components/ridwell/diagnostics.py +++ b/homeassistant/components/ridwell/diagnostics.py @@ -6,19 +6,17 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DATA_COORDINATOR, DOMAIN +from . import RidwellData +from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] + data: RidwellData = hass.data[DOMAIN][entry.entry_id] return { - "data": [dataclasses.asdict(event) for event in coordinator.data.values()], + "data": [dataclasses.asdict(event) for event in data.coordinator.data.values()] } diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index f44cac134d8..9b7a4ab6954 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -18,8 +18,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import RidwellEntity -from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN, SENSOR_TYPE_NEXT_PICKUP +from . import RidwellData, RidwellEntity +from .const import DOMAIN, SENSOR_TYPE_NEXT_PICKUP ATTR_CATEGORY = "category" ATTR_PICKUP_STATE = "pickup_state" @@ -37,13 +37,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - accounts = hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RidwellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - RidwellSensor(coordinator, account, SENSOR_DESCRIPTION) - for account in accounts.values() + RidwellSensor(data.coordinator, account, SENSOR_DESCRIPTION) + for account in data.accounts.values() ] ) diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py index 7bdea622507..d8e228be7db 100644 --- a/homeassistant/components/ridwell/switch.py +++ b/homeassistant/components/ridwell/switch.py @@ -12,8 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RidwellEntity -from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN +from . import RidwellData, RidwellEntity +from .const import DOMAIN SWITCH_TYPE_OPT_IN = "opt_in" @@ -28,13 +28,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - accounts = hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RidwellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - RidwellSwitch(coordinator, account, SWITCH_DESCRIPTION) - for account in accounts.values() + RidwellSwitch(data.coordinator, account, SWITCH_DESCRIPTION) + for account in data.accounts.values() ] ) From 0e59e8b9258bc50029e922085d8a1692eda841d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:24:15 +0200 Subject: [PATCH 574/820] Migrate Moon to new entity naming style (#75085) --- homeassistant/components/moon/sensor.py | 10 +++++++++- tests/components/moon/test_sensor.py | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index c5078771af8..f3a3b3f48fa 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -12,6 +12,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -72,11 +74,17 @@ class MoonSensorEntity(SensorEntity): """Representation of a Moon sensor.""" _attr_device_class = "moon__phase" + _attr_has_entity_name = True + _attr_name = "Phase" def __init__(self, entry: ConfigEntry) -> None: """Initialize the moon sensor.""" - self._attr_name = entry.title self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + name="Moon", + identifiers={(DOMAIN, entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) async def async_update(self) -> None: """Get the time and updates the states.""" diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index bb9e5dcc157..6f13c97a3be 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -16,9 +16,9 @@ from homeassistant.components.moon.sensor import ( STATE_WAXING_CRESCENT, STATE_WAXING_GIBBOUS, ) -from homeassistant.const import ATTR_ICON +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -52,12 +52,20 @@ async def test_moon_day( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.moon") + state = hass.states.get("sensor.moon_phase") assert state assert state.state == native_value assert state.attributes[ATTR_ICON] == icon + assert state.attributes[ATTR_FRIENDLY_NAME] == "Moon Phase" entity_registry = er.async_get(hass) - entry = entity_registry.async_get("sensor.moon") + entry = entity_registry.async_get("sensor.moon_phase") assert entry assert entry.unique_id == mock_config_entry.entry_id + + device_registry = dr.async_get(hass) + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Moon" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE From b62ebbe97431444cdd60edc0532d54f91466f0e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:30:08 +0200 Subject: [PATCH 575/820] Migrate DSMR to new entity naming style (#75077) --- homeassistant/components/dsmr/sensor.py | 65 +++++------ tests/components/dsmr/test_sensor.py | 140 ++++++++++++------------ 2 files changed, 103 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 6acb652c6e5..7ab1a3bb45b 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -78,7 +78,7 @@ class DSMRSensorEntityDescription( SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="current_electricity_usage", - name="Power Consumption", + name="Power consumption", obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE, device_class=SensorDeviceClass.POWER, force_update=True, @@ -86,7 +86,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_delivery", - name="Power Production", + name="Power production", obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, device_class=SensorDeviceClass.POWER, force_update=True, @@ -94,14 +94,14 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_active_tariff", - name="Power Tariff", + name="Active tariff", obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, icon="mdi:flash", ), DSMRSensorEntityDescription( key="electricity_used_tariff_1", - name="Energy Consumption (tarif 1)", + name="Energy consumption (tarif 1)", obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=SensorDeviceClass.ENERGY, @@ -110,7 +110,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_used_tariff_2", - name="Energy Consumption (tarif 2)", + name="Energy consumption (tarif 2)", obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, @@ -119,7 +119,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_delivered_tariff_1", - name="Energy Production (tarif 1)", + name="Energy production (tarif 1)", obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, @@ -128,7 +128,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_delivered_tariff_2", - name="Energy Production (tarif 2)", + name="Energy production (tarif 2)", obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, @@ -137,7 +137,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l1_positive", - name="Power Consumption Phase L1", + name="Power consumption phase L1", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -145,7 +145,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l2_positive", - name="Power Consumption Phase L2", + name="Power consumption phase L2", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -153,7 +153,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l3_positive", - name="Power Consumption Phase L3", + name="Power consumption phase L3", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -161,7 +161,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l1_negative", - name="Power Production Phase L1", + name="Power production phase L1", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -169,7 +169,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l2_negative", - name="Power Production Phase L2", + name="Power production phase L2", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -177,7 +177,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l3_negative", - name="Power Production Phase L3", + name="Power production phase L3", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -185,7 +185,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="short_power_failure_count", - name="Short Power Failure Count", + name="Short power failure count", obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -194,7 +194,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="long_power_failure_count", - name="Long Power Failure Count", + name="Long power failure count", obis_reference=obis_references.LONG_POWER_FAILURE_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -203,7 +203,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_sag_l1_count", - name="Voltage Sags Phase L1", + name="Voltage sags phase L1", obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -211,7 +211,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_sag_l2_count", - name="Voltage Sags Phase L2", + name="Voltage sags phase L2", obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -219,7 +219,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_sag_l3_count", - name="Voltage Sags Phase L3", + name="Voltage sags phase L3", obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -227,7 +227,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_swell_l1_count", - name="Voltage Swells Phase L1", + name="Voltage swells phase L1", obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -236,7 +236,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_swell_l2_count", - name="Voltage Swells Phase L2", + name="Voltage swells phase L2", obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -245,7 +245,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_swell_l3_count", - name="Voltage Swells Phase L3", + name="Voltage swells phase L3", obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -254,7 +254,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_voltage_l1", - name="Voltage Phase L1", + name="Voltage phase L1", obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, @@ -263,7 +263,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_voltage_l2", - name="Voltage Phase L2", + name="Voltage phase L2", obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, @@ -272,7 +272,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_voltage_l3", - name="Voltage Phase L3", + name="Voltage phase L3", obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, @@ -281,7 +281,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_current_l1", - name="Current Phase L1", + name="Current phase L1", obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, @@ -290,7 +290,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_current_l2", - name="Current Phase L2", + name="Current phase L2", obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, @@ -299,7 +299,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_current_l3", - name="Current Phase L3", + name="Current phase L3", obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, @@ -328,7 +328,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_imported_total", - name="Energy Consumption (total)", + name="Energy consumption (total)", obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL, dsmr_versions={"5L", "5S", "Q3D"}, force_update=True, @@ -337,7 +337,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_exported_total", - name="Energy Production (total)", + name="Energy production (total)", obis_reference=obis_references.ELECTRICITY_EXPORTED_TOTAL, dsmr_versions={"5L", "5S", "Q3D"}, force_update=True, @@ -346,7 +346,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="hourly_gas_meter_reading", - name="Gas Consumption", + name="Gas consumption", obis_reference=obis_references.HOURLY_GAS_METER_READING, dsmr_versions={"4", "5", "5L"}, is_gas=True, @@ -356,7 +356,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="belgium_5min_gas_meter_reading", - name="Gas Consumption", + name="Gas consumption", obis_reference=obis_references.BELGIUM_5MIN_GAS_METER_READING, dsmr_versions={"5B"}, is_gas=True, @@ -366,7 +366,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="gas_meter_reading", - name="Gas Consumption", + name="Gas consumption", obis_reference=obis_references.GAS_METER_READING, dsmr_versions={"2.2"}, is_gas=True, @@ -513,6 +513,7 @@ class DSMREntity(SensorEntity): """Entity reading values from DSMR telegram.""" entity_description: DSMRSensorEntityDescription + _attr_has_entity_name = True _attr_should_poll = False def __init__( diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index b006765bde7..e8cf763f98e 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -77,18 +77,18 @@ async def test_default_setup(hass, dsmr_connection_fixture): registry = er.async_get(hass) - entry = registry.async_get("sensor.power_consumption") + entry = registry.async_get("sensor.electricity_meter_power_consumption") assert entry assert entry.unique_id == "1234_current_electricity_usage" - entry = registry.async_get("sensor.gas_consumption") + entry = registry.async_get("sensor.gas_meter_gas_consumption") assert entry assert entry.unique_id == "5678_gas_meter_reading" telegram_callback = connection_factory.call_args_list[0][0][2] # make sure entities have been created and return 'unknown' state - power_consumption = hass.states.get("sensor.power_consumption") + power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") assert power_consumption.state == STATE_UNKNOWN assert ( power_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER @@ -107,22 +107,22 @@ async def test_default_setup(hass, dsmr_connection_fixture): await asyncio.sleep(0) # ensure entities have new state value after incoming telegram - power_consumption = hass.states.get("sensor.power_consumption") + power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") assert power_consumption.state == "0.0" assert ( power_consumption.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR ) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -155,11 +155,11 @@ async def test_setup_only_energy(hass, dsmr_connection_fixture): registry = er.async_get(hass) - entry = registry.async_get("sensor.power_consumption") + entry = registry.async_get("sensor.electricity_meter_power_consumption") assert entry assert entry.unique_id == "1234_current_electricity_usage" - entry = registry.async_get("sensor.gas_consumption") + entry = registry.async_get("sensor.gas_meter_gas_consumption") assert not entry @@ -213,15 +213,15 @@ async def test_v4_meter(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS @@ -284,15 +284,15 @@ async def test_v5_meter(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -359,24 +359,24 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): # after receiving telegram entities need to have the chance to update await asyncio.sleep(0) - power_tariff = hass.states.get("sensor.energy_consumption_total") - assert power_tariff.state == "123.456" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert power_tariff.attributes.get(ATTR_ICON) is None + active_tariff = hass.states.get("sensor.electricity_meter_energy_consumption_total") + assert active_tariff.state == "123.456" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert active_tariff.attributes.get(ATTR_ICON) is None assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) - power_tariff = hass.states.get("sensor.energy_production_total") - assert power_tariff.state == "654.321" - assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") + assert active_tariff.state == "654.321" + assert active_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -438,15 +438,15 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "normal" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "normal" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -497,12 +497,12 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" async def test_swedish_meter(hass, dsmr_connection_fixture): @@ -553,26 +553,26 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): # after receiving telegram entities need to have the chance to update await asyncio.sleep(0) - power_tariff = hass.states.get("sensor.energy_consumption_total") - assert power_tariff.state == "123.456" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert power_tariff.attributes.get(ATTR_ICON) is None + active_tariff = hass.states.get("sensor.electricity_meter_energy_consumption_total") + assert active_tariff.state == "123.456" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert active_tariff.attributes.get(ATTR_ICON) is None assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) - power_tariff = hass.states.get("sensor.energy_production_total") - assert power_tariff.state == "654.321" + active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") + assert active_tariff.state == "654.321" assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) @@ -627,26 +627,26 @@ async def test_easymeter(hass, dsmr_connection_fixture): # after receiving telegram entities need to have the chance to update await asyncio.sleep(0) - power_tariff = hass.states.get("sensor.energy_consumption_total") - assert power_tariff.state == "54184.6316" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert power_tariff.attributes.get(ATTR_ICON) is None + active_tariff = hass.states.get("sensor.electricity_meter_energy_consumption_total") + assert active_tariff.state == "54184.6316" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert active_tariff.attributes.get(ATTR_ICON) is None assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) - power_tariff = hass.states.get("sensor.energy_production_total") - assert power_tariff.state == "19981.1069" + active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") + assert active_tariff.state == "19981.1069" assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) From 05d2b955eedb82113710736c1684e89001ea7eae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:31:17 +0200 Subject: [PATCH 576/820] Migrate CPUSpeed to new entity naming style (#75080) --- homeassistant/components/cpuspeed/sensor.py | 9 ++++++++- tests/components/cpuspeed/test_sensor.py | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 2c9db7e7300..6b64b8709a3 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -7,8 +7,11 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import FREQUENCY_GIGAHERTZ from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import DOMAIN + ATTR_BRAND = "brand" ATTR_HZ = "ghz_advertised" ATTR_ARCH = "arch" @@ -30,12 +33,16 @@ class CPUSpeedSensor(SensorEntity): """Representation of a CPU sensor.""" _attr_icon = "mdi:pulse" - _attr_name = "CPU Speed" + _attr_has_entity_name = True _attr_native_unit_of_measurement = FREQUENCY_GIGAHERTZ def __init__(self, entry: ConfigEntry) -> None: """Initialize the CPU sensor.""" self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + name="CPU Speed", + identifiers={(DOMAIN, entry.entry_id)}, + ) def update(self) -> None: """Get the latest data and updates the state.""" diff --git a/tests/components/cpuspeed/test_sensor.py b/tests/components/cpuspeed/test_sensor.py index ebf9f0111bd..068d24b6f3c 100644 --- a/tests/components/cpuspeed/test_sensor.py +++ b/tests/components/cpuspeed/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +from homeassistant.components.cpuspeed.const import DOMAIN from homeassistant.components.cpuspeed.sensor import ATTR_ARCH, ATTR_BRAND, ATTR_HZ from homeassistant.components.homeassistant import ( DOMAIN as HOME_ASSISTANT_DOMAIN, @@ -15,7 +16,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -29,6 +30,7 @@ async def test_sensor( """Test the CPU Speed sensor.""" await async_setup_component(hass, "homeassistant", {}) entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) entry = entity_registry.async_get("sensor.cpu_speed") assert entry @@ -62,6 +64,12 @@ async def test_sensor( assert state.attributes.get(ATTR_BRAND) == "Intel Ryzen 7" assert state.attributes.get(ATTR_HZ) == 3.6 + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, entry.config_entry_id)} + assert device_entry.name == "CPU Speed" + async def test_sensor_partial_info( hass: HomeAssistant, From 93425b0e4d7e9d13ccb69859e3ae9e6a1ba6219e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:38:00 +0200 Subject: [PATCH 577/820] Migrate Plugwise to new entity naming style (#75109) --- .../components/plugwise/binary_sensor.py | 9 ++- homeassistant/components/plugwise/climate.py | 2 +- homeassistant/components/plugwise/entity.py | 4 +- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 3 +- homeassistant/components/plugwise/select.py | 5 +- homeassistant/components/plugwise/sensor.py | 61 +++++++++---------- homeassistant/components/plugwise/switch.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 45 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 129f4faef92..6f751b82b35 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -31,14 +31,14 @@ class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription): BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( PlugwiseBinarySensorEntityDescription( key="dhw_state", - name="DHW State", + name="DHW state", icon="mdi:water-pump", icon_off="mdi:water-pump-off", entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( key="flame_state", - name="Flame State", + name="Flame state", icon="mdi:fire", icon_off="mdi:fire-off", entity_category=EntityCategory.DIAGNOSTIC, @@ -59,14 +59,14 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( ), PlugwiseBinarySensorEntityDescription( key="slave_boiler_state", - name="Secondary Boiler State", + name="Secondary boiler state", icon="mdi:fire", icon_off="mdi:circle-off-outline", entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( key="plugwise_notification", - name="Plugwise Notification", + name="Plugwise notification", icon="mdi:mailbox-up-outline", icon_off="mdi:mailbox-outline", entity_category=EntityCategory.DIAGNOSTIC, @@ -118,7 +118,6 @@ class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index c74a1baa9be..9729ad745fd 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -39,6 +39,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """Representation of an Plugwise thermostat.""" _attr_temperature_unit = TEMP_CELSIUS + _attr_has_entity_name = True def __init__( self, @@ -49,7 +50,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): super().__init__(coordinator, device_id) self._attr_extra_state_attributes = {} self._attr_unique_id = f"{device_id}-climate" - self._attr_name = self.device.get("name") # Determine preset modes self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index 491eb7c7db8..694f6e5817c 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -18,6 +18,8 @@ from .coordinator import PlugwiseDataUpdateCoordinator class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): """Represent a PlugWise Entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PlugwiseDataUpdateCoordinator, @@ -44,7 +46,7 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): connections=connections, manufacturer=data.get("vendor"), model=data.get("model"), - name=f"Smile {coordinator.data.gateway['smile_name']}", + name=coordinator.data.gateway["smile_name"], sw_version=data.get("firmware"), hw_version=data.get("hardware"), ) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index f0e7f98d4b8..bf7fc453f89 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.18.6"], + "requirements": ["plugwise==0.18.7"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 6718c59c3c0..380be9111ba 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -42,7 +42,7 @@ NUMBER_TYPES = ( key="maximum_boiler_temperature", command=lambda api, value: api.set_max_boiler_temperature(value), device_class=NumberDeviceClass.TEMPERATURE, - name="Maximum Boiler Temperature Setpoint", + name="Maximum boiler temperature setpoint", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=TEMP_CELSIUS, ), @@ -86,7 +86,6 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device['name']} {description.name}").lstrip() self._attr_mode = NumberMode.BOX @property diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index cac9c3e5637..7afe76e1a8a 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -38,7 +38,7 @@ class PlugwiseSelectEntityDescription( SELECT_TYPES = ( PlugwiseSelectEntityDescription( key="select_schedule", - name="Thermostat Schedule", + name="Thermostat schedule", icon="mdi:calendar-clock", command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON), current_option="selected_schedule", @@ -46,7 +46,7 @@ SELECT_TYPES = ( ), PlugwiseSelectEntityDescription( key="select_regulation_mode", - name="Regulation Mode", + name="Regulation mode", icon="mdi:hvac", entity_category=EntityCategory.CONFIG, command=lambda api, loc, opt: api.set_regulation_mode(opt), @@ -92,7 +92,6 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = (f"{self.device['name']} {entity_description.name}").lstrip() @property def current_option(self) -> str: diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 87c81699d10..3ee0437e8dd 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -41,182 +41,182 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="intended_boiler_temperature", - name="Intended Boiler Temperature", + name="Intended boiler temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="temperature_difference", - name="Temperature Difference", + name="Temperature difference", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="outdoor_temperature", - name="Outdoor Temperature", + name="Outdoor temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="outdoor_air_temperature", - name="Outdoor Air Temperature", + name="Outdoor air temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="water_temperature", - name="Water Temperature", + name="Water temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="return_temperature", - name="Return Temperature", + name="Return temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed", - name="Electricity Consumed", + name="Electricity consumed", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced", - name="Electricity Produced", + name="Electricity produced", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_interval", - name="Electricity Consumed Interval", + name="Electricity consumed interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_peak_interval", - name="Electricity Consumed Peak Interval", + name="Electricity consumed peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_off_peak_interval", - name="Electricity Consumed Off Peak Interval", + name="Electricity consumed off peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_interval", - name="Electricity Produced Interval", + name="Electricity produced interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_peak_interval", - name="Electricity Produced Peak Interval", + name="Electricity produced peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_off_peak_interval", - name="Electricity Produced Off Peak Interval", + name="Electricity produced off peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_off_peak_point", - name="Electricity Consumed Off Peak Point", + name="Electricity consumed off peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_peak_point", - name="Electricity Consumed Peak Point", + name="Electricity consumed peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_off_peak_cumulative", - name="Electricity Consumed Off Peak Cumulative", + name="Electricity consumed off peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_consumed_peak_cumulative", - name="Electricity Consumed Peak Cumulative", + name="Electricity consumed peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_produced_off_peak_point", - name="Electricity Produced Off Peak Point", + name="Electricity produced off peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced_peak_point", - name="Electricity Produced Peak Point", + name="Electricity produced peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced_off_peak_cumulative", - name="Electricity Produced Off Peak Cumulative", + name="Electricity produced off peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_produced_peak_cumulative", - name="Electricity Produced Peak Cumulative", + name="Electricity produced peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="gas_consumed_interval", - name="Gas Consumed Interval", + name="Gas consumed interval", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="gas_consumed_cumulative", - name="Gas Consumed Cumulative", + name="Gas consumed cumulative", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="net_electricity_point", - name="Net Electricity Point", + name="Net electricity point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="net_electricity_cumulative", - name="Net Electricity Cumulative", + name="Net electricity cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, @@ -237,28 +237,28 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="modulation_level", - name="Modulation Level", + name="Modulation level", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="valve_position", - name="Valve Position", + name="Valve position", icon="mdi:valve", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="water_pressure", - name="Water Pressure", + name="Water pressure", native_unit_of_measurement=PRESSURE_BAR, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="humidity", - name="Relative Humidity", + name="Relative humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, @@ -307,7 +307,6 @@ class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property def native_value(self) -> int | float | None: diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index 45a10297ed5..c2942308b75 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -21,7 +21,7 @@ from .util import plugwise_command SWITCHES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="dhw_cm_switch", - name="DHW Comfort Mode", + name="DHW comfort mode", icon="mdi:water-plus", entity_category=EntityCategory.CONFIG, ), @@ -68,7 +68,6 @@ class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property def is_on(self) -> bool | None: diff --git a/requirements_all.txt b/requirements_all.txt index 4fb9282d0ce..cee83671b41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1257,7 +1257,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.6 +plugwise==0.18.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ea366315f7..7bab6fbb9d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -871,7 +871,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.6 +plugwise==0.18.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 2db8b154c9ff5b3f8ff935500fdad5c76f287fe4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:39:07 +0200 Subject: [PATCH 578/820] Update orjson to 3.7.8 (#75484) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0d81d485829..d787cec62f2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.7.7 +orjson==3.7.8 paho-mqtt==1.6.1 pillow==9.2.0 pip>=21.0,<22.2 diff --git a/pyproject.toml b/pyproject.toml index 9b4c99c1152..eb1209234be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", - "orjson==3.7.7", + "orjson==3.7.8", "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index ea29bc59435..4785d344e64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.4.0 cryptography==36.0.2 -orjson==3.7.7 +orjson==3.7.8 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 From fe97f6791d938190aebe086e5499153530bd0e02 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:40:34 +0200 Subject: [PATCH 579/820] Map % RH unit in Tuya sensors (#75483) --- homeassistant/components/tuya/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 727e505200b..792036e49ff 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -433,7 +433,7 @@ UNITS = ( ), UnitOfMeasurement( unit=PERCENTAGE, - aliases={"pct", "percent"}, + aliases={"pct", "percent", "% RH"}, device_classes={ SensorDeviceClass.BATTERY, SensorDeviceClass.HUMIDITY, From 460837e453bfb873dc40357d3b870934f48e85e1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:43:46 +0200 Subject: [PATCH 580/820] Allow account linking to phase out services (#75447) --- .../components/cloud/account_link.py | 15 +++++- tests/components/cloud/test_account_link.py | 47 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 5df16cb1724..19819307cf0 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -33,7 +33,20 @@ async def async_provide_implementation(hass: HomeAssistant, domain: str): services = await _get_services(hass) for service in services: - if service["service"] == domain and CURRENT_VERSION >= service["min_version"]: + if ( + service["service"] == domain + and CURRENT_VERSION >= service["min_version"] + and ( + service.get("accepts_new_authorizations", True) + or ( + (entries := hass.config_entries.async_entries(domain)) + and any( + entry.data.get("auth_implementation") == DOMAIN + for entry in entries + ) + ) + ) + ): return [CloudOAuth2Implementation(hass, domain)] return [] diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 16b43d8a3c8..ad914436c07 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -11,7 +11,7 @@ from homeassistant.components.cloud import account_link from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, mock_platform +from tests.common import MockConfigEntry, async_fire_time_changed, mock_platform TEST_DOMAIN = "oauth2_test" @@ -38,6 +38,18 @@ def flow_handler(hass): async def test_setup_provide_implementation(hass): """Test that we provide implementations.""" + legacy_entry = MockConfigEntry( + domain="legacy", + version=1, + data={"auth_implementation": "cloud"}, + ) + none_cloud_entry = MockConfigEntry( + domain="no_cloud", + version=1, + data={"auth_implementation": "somethingelse"}, + ) + none_cloud_entry.add_to_hass(hass) + legacy_entry.add_to_hass(hass) account_link.async_setup(hass) with patch( @@ -45,6 +57,21 @@ async def test_setup_provide_implementation(hass): return_value=[ {"service": "test", "min_version": "0.1.0"}, {"service": "too_new", "min_version": "1000000.0.0"}, + { + "service": "deprecated", + "min_version": "0.1.0", + "accepts_new_authorizations": False, + }, + { + "service": "legacy", + "min_version": "0.1.0", + "accepts_new_authorizations": False, + }, + { + "service": "no_cloud", + "min_version": "0.1.0", + "accepts_new_authorizations": False, + }, ], ): assert ( @@ -57,15 +84,33 @@ async def test_setup_provide_implementation(hass): await config_entry_oauth2_flow.async_get_implementations(hass, "too_new") == {} ) + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, "deprecated") + == {} + ) + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, "no_cloud") + == {} + ) + implementations = await config_entry_oauth2_flow.async_get_implementations( hass, "test" ) + legacy_implementations = ( + await config_entry_oauth2_flow.async_get_implementations(hass, "legacy") + ) + assert "cloud" in implementations assert implementations["cloud"].domain == "cloud" assert implementations["cloud"].service == "test" assert implementations["cloud"].hass is hass + assert "cloud" in legacy_implementations + assert legacy_implementations["cloud"].domain == "cloud" + assert legacy_implementations["cloud"].service == "legacy" + assert legacy_implementations["cloud"].hass is hass + async def test_get_services_cached(hass): """Test that we cache services.""" From 11e7ddaa7141b9ae08e7f15130aea8bc02b4d9df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:58:17 +0200 Subject: [PATCH 581/820] Plugwise prefer use of Adam instead of Anna (#75161) Co-authored-by: Martin Hjelmare --- .../components/plugwise/config_flow.py | 27 ++++++ .../components/plugwise/strings.json | 3 +- tests/components/plugwise/test_config_flow.py | 86 +++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index fbfdca00b41..3fee1445758 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -119,6 +119,32 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): _version = _properties.get("version", "n/a") _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" + # This is an Anna, but we already have config entries. + # Assuming that the user has already configured Adam, aborting discovery. + if self._async_current_entries() and _product == "smile_thermo": + return self.async_abort(reason="anna_with_adam") + + # If we have discovered an Adam or Anna, both might be on the network. + # In that case, we need to cancel the Anna flow, as the Adam should + # be added. + for flow in self._async_in_progress(): + # This is an Anna, and there is already an Adam flow in progress + if ( + _product == "smile_thermo" + and "context" in flow + and flow["context"].get("product") == "smile_open_therm" + ): + return self.async_abort(reason="anna_with_adam") + + # This is an Adam, and there is already an Anna flow in progress + if ( + _product == "smile_open_therm" + and "context" in flow + and flow["context"].get("product") == "smile_thermo" + and "flow_id" in flow + ): + self.hass.config_entries.flow.async_abort(flow["flow_id"]) + self.context.update( { "title_placeholders": { @@ -128,6 +154,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): CONF_USERNAME: self._username, }, "configuration_url": f"http://{discovery_info.host}:{discovery_info.port}", + "product": _product, } ) return await self.async_step_user() diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 42dcee96196..7278f6c4414 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -19,7 +19,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna" } } } diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 66f0986682e..4dbe1d2615f 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -61,6 +61,34 @@ TEST_DISCOVERY2 = ZeroconfServiceInfo( type="mock_type", ) +TEST_DISCOVERY_ANNA = ZeroconfServiceInfo( + host=TEST_HOST, + addresses=[TEST_HOST], + hostname=f"{TEST_HOSTNAME}.local.", + name="mock_name", + port=DEFAULT_PORT, + properties={ + "product": "smile_thermo", + "version": "1.2.3", + "hostname": f"{TEST_HOSTNAME}.local.", + }, + type="mock_type", +) + +TEST_DISCOVERY_ADAM = ZeroconfServiceInfo( + host=TEST_HOST, + addresses=[TEST_HOST], + hostname=f"{TEST_HOSTNAME2}.local.", + name="mock_name", + port=DEFAULT_PORT, + properties={ + "product": "smile_open_therm", + "version": "1.2.3", + "hostname": f"{TEST_HOSTNAME2}.local.", + }, + type="mock_type", +) + @pytest.fixture(name="mock_smile") def mock_smile(): @@ -294,3 +322,61 @@ async def test_flow_errors( assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smile_config_flow.connect.mock_calls) == 2 + + +async def test_zeroconf_abort_anna_with_existing_config_entries( + hass: HomeAssistant, + mock_smile_adam: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test we abort Anna discovery with existing config entries.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ANNA, + ) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "anna_with_adam" + + +async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: + """Test we abort Anna discovery when an Adam is also discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ANNA, + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 1 + assert flows_in_progress[0]["context"]["product"] == "smile_thermo" + + # Discover Adam, Anna should be aborted and no longer present + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ADAM, + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "user" + + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 1 + assert flows_in_progress[0]["context"]["product"] == "smile_open_therm" + + # Discover Anna again, Anna should be aborted directly + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ANNA, + ) + assert result3.get("type") == FlowResultType.ABORT + assert result3.get("reason") == "anna_with_adam" + + # Adam should still be there + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 1 + assert flows_in_progress[0]["context"]["product"] == "smile_open_therm" From 5ef92e5e956de4aada3fbe990f371116f219e7fa Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 20 Jul 2022 11:58:54 +0200 Subject: [PATCH 582/820] Fix MQTT race awaiting an ACK when disconnecting (#75117) Co-authored-by: Erik --- homeassistant/components/mqtt/client.py | 54 +++++++++++++------------ tests/components/mqtt/test_init.py | 46 +++++++++++++++++++-- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 9eeed426d17..6e6f67e4e7a 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -325,11 +325,11 @@ class MQTT: self._ha_started = asyncio.Event() self._last_subscribe = time.time() self._mqttc: mqtt.Client = None - self._paho_lock = asyncio.Lock() - self._pending_acks: set[int] = set() self._cleanup_on_unload: list[Callable] = [] - self._pending_operations: dict[str, asyncio.Event] = {} + self._paho_lock = asyncio.Lock() # Prevents parallel calls to the MQTT client + self._pending_operations: dict[int, asyncio.Event] = {} + self._pending_operations_condition = asyncio.Condition() if self.hass.state == CoreState.running: self._ha_started.set() @@ -431,13 +431,13 @@ class MQTT: # Do not disconnect, we want the broker to always publish will self._mqttc.loop_stop() - # wait for ACK-s to be processes (unsubscribe only) - async with self._paho_lock: - tasks = [ - self.hass.async_create_task(self._wait_for_mid(mid)) - for mid in self._pending_acks - ] - await asyncio.gather(*tasks) + def no_more_acks() -> bool: + """Return False if there are unprocessed ACKs.""" + return not bool(self._pending_operations) + + # wait for ACK-s to be processesed (unsubscribe only) + async with self._pending_operations_condition: + await self._pending_operations_condition.wait_for(no_more_acks) # stop the MQTT loop await self.hass.async_add_executor_job(stop) @@ -487,19 +487,21 @@ class MQTT: This method is a coroutine. """ - def _client_unsubscribe(topic: str) -> None: + def _client_unsubscribe(topic: str) -> int: result: int | None = None result, mid = self._mqttc.unsubscribe(topic) _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) _raise_on_error(result) - self._pending_acks.add(mid) + return mid if any(other.topic == topic for other in self.subscriptions): # Other subscriptions on topic remaining - don't unsubscribe. return async with self._paho_lock: - await self.hass.async_add_executor_job(_client_unsubscribe, topic) + mid = await self.hass.async_add_executor_job(_client_unsubscribe, topic) + await self._register_mid(mid) + self.hass.async_create_task(self._wait_for_mid(mid)) async def _async_perform_subscriptions( self, subscriptions: Iterable[tuple[str, int]] @@ -647,14 +649,18 @@ class MQTT: """Publish / Subscribe / Unsubscribe callback.""" self.hass.add_job(self._mqtt_handle_mid, mid) - @callback - def _mqtt_handle_mid(self, mid) -> None: + async def _mqtt_handle_mid(self, mid: int) -> None: # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() + await self._register_mid(mid) self._pending_operations[mid].set() + async def _register_mid(self, mid: int) -> None: + """Create Event for an expected ACK.""" + async with self._pending_operations_condition: + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: """Disconnected callback.""" self.connected = False @@ -666,12 +672,11 @@ class MQTT: result_code, ) - async def _wait_for_mid(self, mid): + async def _wait_for_mid(self, mid: int) -> None: """Wait for ACK from broker.""" # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() + await self._register_mid(mid) try: await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) except asyncio.TimeoutError: @@ -679,11 +684,10 @@ class MQTT: "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid ) finally: - del self._pending_operations[mid] - # Cleanup ACK sync buffer - async with self._paho_lock: - if mid in self._pending_acks: - self._pending_acks.remove(mid) + async with self._pending_operations_condition: + # Cleanup ACK sync buffer + del self._pending_operations[mid] + self._pending_operations_condition.notify_all() async def _discovery_cooldown(self): now = time.time() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index de63528a08b..cec6038ae04 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -4,7 +4,6 @@ import copy from datetime import datetime, timedelta from functools import partial import json -import logging import ssl from unittest.mock import ANY, AsyncMock, MagicMock, call, mock_open, patch @@ -47,8 +46,6 @@ from tests.common import ( ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES -_LOGGER = logging.getLogger(__name__) - class RecordCallsPartial(partial): """Wrapper class for partial.""" @@ -141,6 +138,49 @@ async def test_mqtt_disconnects_on_home_assistant_stop( assert mqtt_client_mock.loop_stop.call_count == 1 +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_mqtt_await_ack_at_disconnect( + hass, +): + """Test if ACK is awaited correctly when disconnecting.""" + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 100 + rc = 0 + + with patch("paho.mqtt.client.Client") as mock_client: + mock_client().connect = MagicMock(return_value=0) + mock_client().publish = MagicMock(return_value=FakeInfo()) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, + ) + entry.add_to_hass(hass) + assert await mqtt.async_setup_entry(hass, entry) + mqtt_client = mock_client.return_value + + # publish from MQTT client without awaiting + hass.async_create_task( + mqtt.async_publish(hass, "test-topic", "some-payload", 0, False) + ) + await asyncio.sleep(0) + # Simulate late ACK callback from client with mid 100 + mqtt_client.on_publish(0, 0, 100) + # disconnect the MQTT client + await hass.async_stop() + await hass.async_block_till_done() + # assert the payload was sent through the client + assert mqtt_client.publish.called + assert mqtt_client.publish.call_args[0] == ( + "test-topic", + "some-payload", + 0, + False, + ) + + async def test_publish(hass, mqtt_mock_entry_no_yaml_config): """Test the publish function.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() From a3b2b5c328a5e17929b2bcd54c9ed3c5acb44a36 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 12:03:30 +0200 Subject: [PATCH 583/820] Add zha typing [classmethods] (#75472) --- homeassistant/components/zha/button.py | 14 +++++---- homeassistant/components/zha/core/device.py | 8 +++-- homeassistant/components/zha/entity.py | 29 ++++++++++++----- homeassistant/components/zha/number.py | 14 ++++++--- homeassistant/components/zha/select.py | 16 ++++++---- homeassistant/components/zha/sensor.py | 35 ++++++++++++--------- homeassistant/components/zha/switch.py | 10 ++++-- 7 files changed, 81 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 0f98bfaad51..fcc040cbde2 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -4,7 +4,7 @@ from __future__ import annotations import abc import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.foundation import Status @@ -27,6 +27,8 @@ if TYPE_CHECKING: from .core.device import ZHADevice +_ZHAIdentifyButtonSelfT = TypeVar("_ZHAIdentifyButtonSelfT", bound="ZHAIdentifyButton") + MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.BUTTON) CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.BUTTON @@ -66,7 +68,7 @@ class ZHAButton(ZhaEntity, ButtonEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this button.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -89,12 +91,12 @@ class ZHAIdentifyButton(ZHAButton): @classmethod def create_entity( - cls, + cls: type[_ZHAIdentifyButtonSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZHAIdentifyButtonSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -126,7 +128,7 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this button.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 4719b6bf585..150241a091b 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -9,7 +9,7 @@ from functools import cached_property import logging import random import time -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from zigpy import types import zigpy.device @@ -86,6 +86,8 @@ _LOGGER = logging.getLogger(__name__) _UPDATE_ALIVE_INTERVAL = (60, 90) _CHECKIN_GRACE_PERIODS = 2 +_ZHADeviceSelfT = TypeVar("_ZHADeviceSelfT", bound="ZHADevice") + class DeviceStatus(Enum): """Status of a device.""" @@ -340,12 +342,12 @@ class ZHADevice(LogMixin): @classmethod def new( - cls, + cls: type[_ZHADeviceSelfT], hass: HomeAssistant, zigpy_dev: zigpy.device.Device, gateway: ZHAGateway, restored: bool = False, - ): + ) -> _ZHADeviceSelfT: """Create new device.""" zha_dev = cls(hass, zigpy_dev, gateway) zha_dev.channels = channels.Channels.new(zha_dev) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 8b3627df9de..2f609555c79 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Callable import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from homeassistant.const import ATTR_NAME from homeassistant.core import CALLBACK_TYPE, Event, callback @@ -35,6 +35,9 @@ if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel from .core.device import ZHADevice +_ZhaEntitySelfT = TypeVar("_ZhaEntitySelfT", bound="ZhaEntity") +_ZhaGroupEntitySelfT = TypeVar("_ZhaGroupEntitySelfT", bound="ZhaGroupEntity") + _LOGGER = logging.getLogger(__name__) ENTITY_SUFFIX = "entity_suffix" @@ -155,7 +158,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): class ZhaEntity(BaseZhaEntity, RestoreEntity): """A base class for non group ZHA entities.""" - def __init_subclass__(cls, id_suffix: str | None = None, **kwargs) -> None: + def __init_subclass__(cls, id_suffix: str | None = None, **kwargs: Any) -> None: """Initialize subclass. :param id_suffix: suffix to add to the unique_id of the entity. Used for multi @@ -187,12 +190,12 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): @classmethod def create_entity( - cls, + cls: type[_ZhaEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZhaEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -257,7 +260,12 @@ class ZhaGroupEntity(BaseZhaEntity): """A base class for ZHA group entities.""" def __init__( - self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs + self, + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, ) -> None: """Initialize a light group.""" super().__init__(unique_id, zha_device, **kwargs) @@ -279,8 +287,13 @@ class ZhaGroupEntity(BaseZhaEntity): @classmethod def create_entity( - cls, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs - ) -> ZhaGroupEntity | None: + cls: type[_ZhaGroupEntitySelfT], + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, + ) -> _ZhaGroupEntitySelfT | None: """Group Entity Factory. Return entity if it is a supported configuration, otherwise return None diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 76b1121c2f0..4252bf0e14c 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.foundation import Status @@ -33,6 +33,10 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +_ZHANumberConfigurationEntitySelfT = TypeVar( + "_ZHANumberConfigurationEntitySelfT", bound="ZHANumberConfigurationEntity" +) + STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.NUMBER) CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.NUMBER @@ -368,12 +372,12 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): @classmethod def create_entity( - cls, + cls: type[_ZHANumberConfigurationEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZHANumberConfigurationEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -397,7 +401,7 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this number configuration entity.""" self._channel: ZigbeeChannel = channels[0] diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index e2835d4acd4..503c5a013a8 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum import functools import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeVar from zigpy import types from zigpy.zcl.clusters.general import OnOff @@ -34,6 +34,10 @@ if TYPE_CHECKING: from .core.device import ZHADevice +_ZCLEnumSelectEntitySelfT = TypeVar( + "_ZCLEnumSelectEntitySelfT", bound="ZCLEnumSelectEntity" +) + CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.SELECT ) @@ -72,7 +76,7 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this select entity.""" self._attr_name = self._enum.__name__ @@ -154,12 +158,12 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): @classmethod def create_entity( - cls, + cls: type[_ZCLEnumSelectEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZCLEnumSelectEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -183,7 +187,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this select entity.""" self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 513ba5510b5..f42e88041ef 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import numbers -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from homeassistant.components.climate.const import HVACAction from homeassistant.components.sensor import ( @@ -69,6 +69,13 @@ if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel from .core.device import ZHADevice +_SensorSelfT = TypeVar("_SensorSelfT", bound="Sensor") +_BatterySelfT = TypeVar("_BatterySelfT", bound="Battery") +_ThermostatHVACActionSelfT = TypeVar( + "_ThermostatHVACActionSelfT", bound="ThermostatHVACAction" +) +_RSSISensorSelfT = TypeVar("_RSSISensorSelfT", bound="RSSISensor") + PARALLEL_UPDATES = 5 BATTERY_SIZES = { @@ -126,7 +133,7 @@ class Sensor(ZhaEntity, SensorEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -134,12 +141,12 @@ class Sensor(ZhaEntity, SensorEntity): @classmethod def create_entity( - cls, + cls: type[_SensorSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _SensorSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -214,12 +221,12 @@ class Battery(Sensor): @classmethod def create_entity( - cls, + cls: type[_BatterySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _BatterySelfT | None: """Entity Factory. Unlike any other entity, PowerConfiguration cluster may not support @@ -641,12 +648,12 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): @classmethod def create_entity( - cls, + cls: type[_ThermostatHVACActionSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ThermostatHVACActionSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -767,12 +774,12 @@ class RSSISensor(Sensor, id_suffix="rssi"): @classmethod def create_entity( - cls, + cls: type[_RSSISensorSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _RSSISensorSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 3b044bb7646..881401f31da 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.clusters.general import OnOff @@ -31,6 +31,10 @@ if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel from .core.device import ZHADevice +_ZHASwitchConfigurationEntitySelfT = TypeVar( + "_ZHASwitchConfigurationEntitySelfT", bound="ZHASwitchConfigurationEntity" +) + STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.SWITCH) CONFIG_DIAGNOSTIC_MATCH = functools.partial( @@ -172,12 +176,12 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): @classmethod def create_entity( - cls, + cls: type[_ZHASwitchConfigurationEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], **kwargs: Any, - ) -> ZhaEntity | None: + ) -> _ZHASwitchConfigurationEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None From 39dc9aa1795c25599cc794faf216d0b4ca950fd4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 12:06:52 +0200 Subject: [PATCH 584/820] Rename Resolution Center -> Repairs (#75486) --- .strict-typing | 2 +- CODEOWNERS | 4 +- .../__init__.py | 8 +-- homeassistant/components/repairs/const.py | 3 + .../issue_handler.py | 38 ++++++------- .../issue_registry.py | 2 +- .../manifest.json | 6 +- .../{resolution_center => repairs}/models.py | 10 ++-- .../websocket_api.py | 28 ++++----- .../components/resolution_center/const.py | 3 - mypy.ini | 2 +- script/hassfest/manifest.py | 2 +- tests/components/repairs/__init__.py | 1 + .../test_init.py | 51 ++++++++--------- .../test_issue_registry.py | 11 ++-- .../test_websocket_api.py | 57 +++++++++---------- .../components/resolution_center/__init__.py | 1 - 17 files changed, 106 insertions(+), 123 deletions(-) rename homeassistant/components/{resolution_center => repairs}/__init__.py (66%) create mode 100644 homeassistant/components/repairs/const.py rename homeassistant/components/{resolution_center => repairs}/issue_handler.py (75%) rename homeassistant/components/{resolution_center => repairs}/issue_registry.py (99%) rename homeassistant/components/{resolution_center => repairs}/manifest.json (65%) rename homeassistant/components/{resolution_center => repairs}/models.py (69%) rename homeassistant/components/{resolution_center => repairs}/websocket_api.py (83%) delete mode 100644 homeassistant/components/resolution_center/const.py create mode 100644 tests/components/repairs/__init__.py rename tests/components/{resolution_center => repairs}/test_init.py (87%) rename tests/components/{resolution_center => repairs}/test_issue_registry.py (91%) rename tests/components/{resolution_center => repairs}/test_websocket_api.py (86%) delete mode 100644 tests/components/resolution_center/__init__.py diff --git a/.strict-typing b/.strict-typing index 46d85aa1ec2..7616589a7b5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -197,7 +197,7 @@ homeassistant.components.recollect_waste.* homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* -homeassistant.components.resolution_center.* +homeassistant.components.repairs.* homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* diff --git a/CODEOWNERS b/CODEOWNERS index fd271e6adc6..dbbb21f0d6c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -859,9 +859,9 @@ build.json @home-assistant/supervisor /tests/components/remote/ @home-assistant/core /homeassistant/components/renault/ @epenet /tests/components/renault/ @epenet +/homeassistant/components/repairs/ @home-assistant/core +/tests/components/repairs/ @home-assistant/core /homeassistant/components/repetier/ @MTrab @ShadowBr0ther -/homeassistant/components/resolution_center/ @home-assistant/core -/tests/components/resolution_center/ @home-assistant/core /homeassistant/components/rflink/ @javicalle /tests/components/rflink/ @javicalle /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/resolution_center/__init__.py b/homeassistant/components/repairs/__init__.py similarity index 66% rename from homeassistant/components/resolution_center/__init__.py rename to homeassistant/components/repairs/__init__.py index 7d0cd8416c3..5a85d1999d0 100644 --- a/homeassistant/components/resolution_center/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -1,4 +1,4 @@ -"""The resolution center integration.""" +"""The repairs integration.""" from __future__ import annotations from homeassistant.core import HomeAssistant @@ -6,14 +6,14 @@ from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api from .const import DOMAIN -from .issue_handler import ResolutionCenterFlow, async_create_issue, async_delete_issue +from .issue_handler import RepairsFlow, async_create_issue, async_delete_issue from .issue_registry import async_load as async_load_issue_registry -__all__ = ["DOMAIN", "ResolutionCenterFlow", "async_create_issue", "async_delete_issue"] +__all__ = ["DOMAIN", "RepairsFlow", "async_create_issue", "async_delete_issue"] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up Resolution Center.""" + """Set up Repairs.""" hass.data[DOMAIN] = {} issue_handler.async_setup(hass) diff --git a/homeassistant/components/repairs/const.py b/homeassistant/components/repairs/const.py new file mode 100644 index 00000000000..cddc5edcffd --- /dev/null +++ b/homeassistant/components/repairs/const.py @@ -0,0 +1,3 @@ +"""Constants for the Repairs integration.""" + +DOMAIN = "repairs" diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/repairs/issue_handler.py similarity index 75% rename from homeassistant/components/resolution_center/issue_handler.py rename to homeassistant/components/repairs/issue_handler.py index 78085e2cec6..ff23c1c70a4 100644 --- a/homeassistant/components/resolution_center/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -1,4 +1,4 @@ -"""The resolution center integration.""" +"""The repairs integration.""" from __future__ import annotations from typing import Any @@ -14,11 +14,11 @@ from homeassistant.helpers.integration_platform import ( from .const import DOMAIN from .issue_registry import async_get as async_get_issue_registry -from .models import IssueSeverity, ResolutionCenterFlow, ResolutionCenterProtocol +from .models import IssueSeverity, RepairsFlow, RepairsProtocol -class ResolutionCenterFlowManager(data_entry_flow.FlowManager): - """Manage resolution center flows.""" +class RepairsFlowManager(data_entry_flow.FlowManager): + """Manage repairs flows.""" async def async_create_flow( self, @@ -26,14 +26,12 @@ class ResolutionCenterFlowManager(data_entry_flow.FlowManager): *, context: dict[str, Any] | None = None, data: dict[str, Any] | None = None, - ) -> ResolutionCenterFlow: - """Create a flow. platform is a resolution center module.""" + ) -> RepairsFlow: + """Create a flow. platform is a repairs module.""" if "platforms" not in self.hass.data[DOMAIN]: - await async_process_resolution_center_platforms(self.hass) + await async_process_repairs_platforms(self.hass) - platforms: dict[str, ResolutionCenterProtocol] = self.hass.data[DOMAIN][ - "platforms" - ] + platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"] if handler_key not in platforms: raise data_entry_flow.UnknownHandler platform = platforms[handler_key] @@ -60,25 +58,23 @@ class ResolutionCenterFlowManager(data_entry_flow.FlowManager): @callback def async_setup(hass: HomeAssistant) -> None: - """Initialize resolution center.""" - hass.data[DOMAIN]["flow_manager"] = ResolutionCenterFlowManager(hass) + """Initialize repairs.""" + hass.data[DOMAIN]["flow_manager"] = RepairsFlowManager(hass) -async def async_process_resolution_center_platforms(hass: HomeAssistant) -> None: - """Start processing resolution center platforms.""" +async def async_process_repairs_platforms(hass: HomeAssistant) -> None: + """Start processing repairs platforms.""" hass.data[DOMAIN]["platforms"] = {} - await async_process_integration_platforms( - hass, DOMAIN, _register_resolution_center_platform - ) + await async_process_integration_platforms(hass, DOMAIN, _register_repairs_platform) -async def _register_resolution_center_platform( - hass: HomeAssistant, integration_domain: str, platform: ResolutionCenterProtocol +async def _register_repairs_platform( + hass: HomeAssistant, integration_domain: str, platform: RepairsProtocol ) -> None: - """Register a resolution center platform.""" + """Register a repairs platform.""" if not hasattr(platform, "async_create_fix_flow"): - raise HomeAssistantError(f"Invalid resolution center platform {platform}") + raise HomeAssistantError(f"Invalid repairs platform {platform}") hass.data[DOMAIN]["platforms"][integration_domain] = platform diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/repairs/issue_registry.py similarity index 99% rename from homeassistant/components/resolution_center/issue_registry.py rename to homeassistant/components/repairs/issue_registry.py index e398d58b3e0..c1eda0d53be 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -13,7 +13,7 @@ import homeassistant.util.dt as dt_util from .models import IssueSeverity DATA_REGISTRY = "issue_registry" -STORAGE_KEY = "resolution_center.issue_registry" +STORAGE_KEY = "repairs.issue_registry" STORAGE_VERSION = 1 SAVE_DELAY = 10 diff --git a/homeassistant/components/resolution_center/manifest.json b/homeassistant/components/repairs/manifest.json similarity index 65% rename from homeassistant/components/resolution_center/manifest.json rename to homeassistant/components/repairs/manifest.json index 4b8c1bb2506..f2013743a05 100644 --- a/homeassistant/components/resolution_center/manifest.json +++ b/homeassistant/components/repairs/manifest.json @@ -1,8 +1,8 @@ { - "domain": "resolution_center", - "name": "Resolution Center", + "domain": "repairs", + "name": "Repairs", "config_flow": false, - "documentation": "https://www.home-assistant.io/integrations/resolution_center", + "documentation": "https://www.home-assistant.io/integrations/repairs", "codeowners": ["@home-assistant/core"], "dependencies": ["http"] } diff --git a/homeassistant/components/resolution_center/models.py b/homeassistant/components/repairs/models.py similarity index 69% rename from homeassistant/components/resolution_center/models.py rename to homeassistant/components/repairs/models.py index 12e2f4e73f3..2a6eeb15269 100644 --- a/homeassistant/components/resolution_center/models.py +++ b/homeassistant/components/repairs/models.py @@ -1,4 +1,4 @@ -"""Models for Resolution Center.""" +"""Models for Repairs.""" from __future__ import annotations from typing import Protocol @@ -16,14 +16,14 @@ class IssueSeverity(StrEnum): WARNING = "warning" -class ResolutionCenterFlow(data_entry_flow.FlowHandler): +class RepairsFlow(data_entry_flow.FlowHandler): """Handle a flow for fixing an issue.""" -class ResolutionCenterProtocol(Protocol): - """Define the format of resolution center platforms.""" +class RepairsProtocol(Protocol): + """Define the format of repairs platforms.""" async def async_create_fix_flow( self, hass: HomeAssistant, issue_id: str - ) -> ResolutionCenterFlow: + ) -> RepairsFlow: """Create a flow to fix a fixable issue.""" diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/repairs/websocket_api.py similarity index 83% rename from homeassistant/components/resolution_center/websocket_api.py rename to homeassistant/components/repairs/websocket_api.py index e111c2b3e50..2e4e166f3e9 100644 --- a/homeassistant/components/resolution_center/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -1,4 +1,4 @@ -"""The resolution center websocket API.""" +"""The repairs websocket API.""" from __future__ import annotations import dataclasses @@ -26,22 +26,18 @@ from .issue_registry import async_get as async_get_issue_registry @callback def async_setup(hass: HomeAssistant) -> None: - """Set up the resolution center websocket API.""" + """Set up the repairs websocket API.""" websocket_api.async_register_command(hass, ws_ignore_issue) websocket_api.async_register_command(hass, ws_list_issues) - hass.http.register_view( - ResolutionCenterFlowIndexView(hass.data[DOMAIN]["flow_manager"]) - ) - hass.http.register_view( - ResolutionCenterFlowResourceView(hass.data[DOMAIN]["flow_manager"]) - ) + hass.http.register_view(RepairsFlowIndexView(hass.data[DOMAIN]["flow_manager"])) + hass.http.register_view(RepairsFlowResourceView(hass.data[DOMAIN]["flow_manager"])) @callback @websocket_api.websocket_command( { - vol.Required("type"): "resolution_center/ignore_issue", + vol.Required("type"): "repairs/ignore_issue", vol.Required("domain"): str, vol.Required("issue_id"): str, vol.Required("ignore"): bool, @@ -58,7 +54,7 @@ def ws_ignore_issue( @websocket_api.websocket_command( { - vol.Required("type"): "resolution_center/list_issues", + vol.Required("type"): "repairs/list_issues", } ) @callback @@ -82,11 +78,11 @@ def ws_list_issues( connection.send_result(msg["id"], {"issues": issues}) -class ResolutionCenterFlowIndexView(FlowManagerIndexView): +class RepairsFlowIndexView(FlowManagerIndexView): """View to create issue fix flows.""" - url = "/api/resolution_center/issues/fix" - name = "api:resolution_center:issues:fix" + url = "/api/repairs/issues/fix" + name = "api:repairs:issues:fix" @RequestDataValidator( vol.Schema( @@ -119,11 +115,11 @@ class ResolutionCenterFlowIndexView(FlowManagerIndexView): return self.json(result) # pylint: disable=arguments-differ -class ResolutionCenterFlowResourceView(FlowManagerResourceView): +class RepairsFlowResourceView(FlowManagerResourceView): """View to interact with the option flow manager.""" - url = "/api/resolution_center/issues/fix/{flow_id}" - name = "api:resolution_center:issues:fix:resource" + url = "/api/repairs/issues/fix/{flow_id}" + name = "api:repairs:issues:fix:resource" async def get(self, request: web.Request, flow_id: str) -> web.Response: """Get the current state of a data_entry_flow.""" diff --git a/homeassistant/components/resolution_center/const.py b/homeassistant/components/resolution_center/const.py deleted file mode 100644 index 46d020e5118..00000000000 --- a/homeassistant/components/resolution_center/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants for the Resolution Center integration.""" - -DOMAIN = "resolution_center" diff --git a/mypy.ini b/mypy.ini index 04b76dfea0f..84bc6ae00b0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1898,7 +1898,7 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.resolution_center.*] +[mypy-homeassistant.components.repairs.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 9c1bd0a63f3..129899cff11 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -80,7 +80,7 @@ NO_IOT_CLASS = [ "proxy", "python_script", "raspberry_pi", - "resolution_center", + "repairs", "safe_mode", "script", "search", diff --git a/tests/components/repairs/__init__.py b/tests/components/repairs/__init__.py new file mode 100644 index 00000000000..b0d26f49ee4 --- /dev/null +++ b/tests/components/repairs/__init__.py @@ -0,0 +1 @@ +"""Tests for the repairs integration.""" diff --git a/tests/components/resolution_center/test_init.py b/tests/components/repairs/test_init.py similarity index 87% rename from tests/components/resolution_center/test_init.py rename to tests/components/repairs/test_init.py index 478c2d51f7f..fbf240e740d 100644 --- a/tests/components/resolution_center/test_init.py +++ b/tests/components/repairs/test_init.py @@ -1,17 +1,14 @@ -"""Test the resolution center websocket API.""" +"""Test the repairs websocket API.""" from unittest.mock import AsyncMock, Mock from freezegun import freeze_time import pytest -from homeassistant.components.resolution_center import ( - async_create_issue, - async_delete_issue, -) -from homeassistant.components.resolution_center.const import DOMAIN -from homeassistant.components.resolution_center.issue_handler import ( +from homeassistant.components.repairs import async_create_issue, async_delete_issue +from homeassistant.components.repairs.const import DOMAIN +from homeassistant.components.repairs.issue_handler import ( async_ignore_issue, - async_process_resolution_center_platforms, + async_process_repairs_platforms, ) from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant @@ -27,7 +24,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: client = await hass_ws_client(hass) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -69,7 +66,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -98,7 +95,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issues[0]["translation_placeholders"], ) - await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 3, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -144,7 +141,7 @@ async def test_create_issue_invalid_version( translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -158,7 +155,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: client = await hass_ws_client(hass) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -190,7 +187,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -210,7 +207,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: with pytest.raises(KeyError): async_ignore_issue(hass, issues[0]["domain"], "no_such_issue", True) - await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 3, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -229,7 +226,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: # Ignore an existing issue async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) - await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 4, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -248,7 +245,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: # Ignore the same issue again async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) - await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 5, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -277,7 +274,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issues[0]["translation_placeholders"], ) - await client.send_json({"id": 6, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 6, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -292,7 +289,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: # Unignore the same issue async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], False) - await client.send_json({"id": 7, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 7, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -343,7 +340,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -362,7 +359,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non # Delete a non-existing issue async_delete_issue(hass, issues[0]["domain"], "no_such_issue") - await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -381,7 +378,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non # Delete an existing issue async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) - await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 3, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -390,7 +387,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non # Delete the same issue again async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) - await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 4, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -412,7 +409,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 5, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -436,16 +433,16 @@ async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> No hass.config.components.add("integration_without_diagnostics") mock_platform( hass, - "fake_integration.resolution_center", + "fake_integration.repairs", Mock(async_create_fix_flow=AsyncMock(return_value=True)), ) mock_platform( hass, - "integration_without_diagnostics.resolution_center", + "integration_without_diagnostics.repairs", Mock(spec=[]), ) assert await async_setup_component(hass, DOMAIN, {}) - await async_process_resolution_center_platforms(hass) + await async_process_repairs_platforms(hass) assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"] diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/repairs/test_issue_registry.py similarity index 91% rename from tests/components/resolution_center/test_issue_registry.py rename to tests/components/repairs/test_issue_registry.py index 854a14fc84e..1af67601581 100644 --- a/tests/components/resolution_center/test_issue_registry.py +++ b/tests/components/repairs/test_issue_registry.py @@ -1,10 +1,7 @@ -"""Test the resolution center websocket API.""" -from homeassistant.components.resolution_center import ( - async_create_issue, - issue_registry, -) -from homeassistant.components.resolution_center.const import DOMAIN -from homeassistant.components.resolution_center.issue_handler import async_ignore_issue +"""Test the repairs websocket API.""" +from homeassistant.components.repairs import async_create_issue, issue_registry +from homeassistant.components.repairs.const import DOMAIN +from homeassistant.components.repairs.issue_handler import async_ignore_issue from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py similarity index 86% rename from tests/components/resolution_center/test_websocket_api.py rename to tests/components/repairs/test_websocket_api.py index 044b0e9832b..388912e0adc 100644 --- a/tests/components/resolution_center/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -1,4 +1,4 @@ -"""Test the resolution center websocket API.""" +"""Test the repairs websocket API.""" from __future__ import annotations from http import HTTPStatus @@ -9,11 +9,8 @@ import pytest import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.resolution_center import ( - ResolutionCenterFlow, - async_create_issue, -) -from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.components.repairs import RepairsFlow, async_create_issue +from homeassistant.components.repairs.const import DOMAIN from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -49,7 +46,7 @@ async def create_issues(hass, ws_client): translation_placeholders=issue["translation_placeholders"], ) - await ws_client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await ws_client.receive_json() assert msg["success"] @@ -68,7 +65,7 @@ async def create_issues(hass, ws_client): return issues -class MockFixFlow(ResolutionCenterFlow): +class MockFixFlow(RepairsFlow): """Handler for an issue fixing flow.""" async def async_step_init( @@ -89,8 +86,8 @@ class MockFixFlow(ResolutionCenterFlow): @pytest.fixture(autouse=True) -async def mock_resolution_center_integration(hass): - """Mock a resolution_center integration.""" +async def mock_repairs_integration(hass): + """Mock a repairs integration.""" hass.config.components.add("fake_integration") hass.config.components.add("integration_without_diagnostics") @@ -99,12 +96,12 @@ async def mock_resolution_center_integration(hass): mock_platform( hass, - "fake_integration.resolution_center", + "fake_integration.repairs", Mock(async_create_fix_flow=AsyncMock(wraps=async_create_fix_flow)), ) mock_platform( hass, - "integration_without_diagnostics.resolution_center", + "integration_without_diagnostics.repairs", Mock(spec=[]), ) @@ -120,7 +117,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 2, - "type": "resolution_center/ignore_issue", + "type": "repairs/ignore_issue", "domain": "fake_integration", "issue_id": "no_such_issue", "ignore": True, @@ -132,7 +129,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 3, - "type": "resolution_center/ignore_issue", + "type": "repairs/ignore_issue", "domain": "fake_integration", "issue_id": "issue_1", "ignore": True, @@ -142,7 +139,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"] is None - await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 4, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -161,7 +158,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 5, - "type": "resolution_center/ignore_issue", + "type": "repairs/ignore_issue", "domain": "fake_integration", "issue_id": "issue_1", "ignore": False, @@ -171,7 +168,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"] is None - await client.send_json({"id": 6, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 6, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -200,21 +197,21 @@ async def test_fix_non_existing_issue( issues = await create_issues(hass, ws_client) - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "no_such_integration", "issue_id": "no_such_issue"} ) assert resp.status != HTTPStatus.OK - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "no_such_issue"} ) assert resp.status != HTTPStatus.OK - await ws_client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + await ws_client.send_json({"id": 3, "type": "repairs/list_issues"}) msg = await ws_client.receive_json() assert msg["success"] @@ -241,7 +238,7 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No await create_issues(hass, ws_client) - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "issue_1"} ) @@ -261,7 +258,7 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No "type": "form", } - url = f"/api/resolution_center/issues/fix/{flow_id}" + url = f"/api/repairs/issues/fix/{flow_id}" # Test we can get the status of the flow resp2 = await client.get(url) @@ -286,7 +283,7 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No "version": 1, } - await ws_client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) msg = await ws_client.receive_json() assert msg["success"] @@ -304,7 +301,7 @@ async def test_fix_issue_unauth( client = await hass_client() - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "issue_1"} ) @@ -324,7 +321,7 @@ async def test_get_progress_unauth( await create_issues(hass, ws_client) - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "issue_1"} ) @@ -334,7 +331,7 @@ async def test_get_progress_unauth( hass_admin_user.groups = [] - url = f"/api/resolution_center/issues/fix/{flow_id}" + url = f"/api/repairs/issues/fix/{flow_id}" # Test we can't get the status of the flow resp = await client.get(url) assert resp.status == HTTPStatus.UNAUTHORIZED @@ -352,7 +349,7 @@ async def test_step_unauth( await create_issues(hass, ws_client) - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "issue_1"} ) @@ -362,7 +359,7 @@ async def test_step_unauth( hass_admin_user.groups = [] - url = f"/api/resolution_center/issues/fix/{flow_id}" + url = f"/api/repairs/issues/fix/{flow_id}" # Test we can't get the status of the flow resp = await client.post(url) assert resp.status == HTTPStatus.UNAUTHORIZED @@ -375,7 +372,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: client = await hass_ws_client(hass) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -417,7 +414,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] diff --git a/tests/components/resolution_center/__init__.py b/tests/components/resolution_center/__init__.py deleted file mode 100644 index a6a86bf99bc..00000000000 --- a/tests/components/resolution_center/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the resolution center integration.""" From 712492b0665240d933206d402d5583fad2bd60ab Mon Sep 17 00:00:00 2001 From: Zach Berger Date: Wed, 20 Jul 2022 03:08:34 -0700 Subject: [PATCH 585/820] Update awair SensorDeviceClass to specify icon (#75385) --- homeassistant/components/awair/const.py | 7 +++---- tests/components/awair/test_sensor.py | 23 ++++++++--------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 5c2a3da89ce..6fcf63abb4d 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -86,7 +86,7 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( ), AwairSensorEntityDescription( key=API_VOC, - icon="mdi:cloud", + icon="mdi:molecule", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, name="Volatile organic compounds", unique_id_tag="VOC", # matches legacy format @@ -101,7 +101,6 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( AwairSensorEntityDescription( key=API_CO2, device_class=SensorDeviceClass.CO2, - icon="mdi:cloud", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, name="Carbon dioxide", unique_id_tag="CO2", # matches legacy format @@ -111,14 +110,14 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = ( AwairSensorEntityDescription( key=API_PM25, - icon="mdi:blur", + device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name="PM2.5", unique_id_tag="PM25", # matches legacy format ), AwairSensorEntityDescription( key=API_PM10, - icon="mdi:blur", + device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name="PM10", unique_id_tag="PM10", # matches legacy format diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 5f30be81d6f..07b2f9ba00f 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -17,7 +17,6 @@ from homeassistant.components.awair.const import ( SENSOR_TYPES_DUST, ) from homeassistant.const import ( - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, @@ -88,7 +87,7 @@ async def test_awair_gen1_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "88", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -116,7 +115,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_CO2].unique_id_tag}", "654.0", { - ATTR_ICON: "mdi:cloud", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, "awair_index": 0.0, }, @@ -129,7 +127,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_VOC].unique_id_tag}", "366", { - ATTR_ICON: "mdi:cloud", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, "awair_index": 1.0, }, @@ -143,7 +140,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_DUST", "14.3", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 1.0, }, @@ -156,7 +152,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM10].unique_id_tag}", "14.3", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 1.0, }, @@ -184,7 +179,7 @@ async def test_awair_gen2_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "97", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -194,7 +189,6 @@ async def test_awair_gen2_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM25].unique_id_tag}", "2.0", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 0.0, }, @@ -218,7 +212,7 @@ async def test_awair_mint_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "98", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -228,7 +222,6 @@ async def test_awair_mint_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM25].unique_id_tag}", "1.0", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 0.0, }, @@ -260,7 +253,7 @@ async def test_awair_glow_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "93", - {ATTR_ICON: "mdi:blur"}, + {}, ) # The glow does not have a particle sensor @@ -280,7 +273,7 @@ async def test_awair_omni_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "99", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -289,7 +282,7 @@ async def test_awair_omni_sensors(hass): "sensor.living_room_sound_level", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SPL_A].unique_id_tag}", "47.0", - {ATTR_ICON: "mdi:ear-hearing", ATTR_UNIT_OF_MEASUREMENT: "dBa"}, + {ATTR_UNIT_OF_MEASUREMENT: "dBa"}, ) assert_expected_properties( @@ -333,7 +326,7 @@ async def test_awair_unavailable(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "88", - {ATTR_ICON: "mdi:blur"}, + {}, ) with patch("python_awair.AwairClient.query", side_effect=OFFLINE_FIXTURE): @@ -344,5 +337,5 @@ async def test_awair_unavailable(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", STATE_UNAVAILABLE, - {ATTR_ICON: "mdi:blur"}, + {}, ) From 95e07508e54e31493911889c2adbba5997616a64 Mon Sep 17 00:00:00 2001 From: Pascal Winters Date: Wed, 20 Jul 2022 12:57:00 +0200 Subject: [PATCH 586/820] Bump pySwitchbot to 0.14.1 (#75487) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 91ee8881a07..ccce534c6df 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.14.0"], + "requirements": ["PySwitchbot==0.14.1"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], diff --git a/requirements_all.txt b/requirements_all.txt index cee83671b41..b3395085f2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.0 +PySwitchbot==0.14.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7bab6fbb9d6..f53ea0fd4cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.0 +PySwitchbot==0.14.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From cd3e99564f3d60d1edef67a479c2c435126a784a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 13:02:13 +0200 Subject: [PATCH 587/820] Add repairs integration to core files (#75489) --- .core_files.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.core_files.yaml b/.core_files.yaml index 29390865611..df69df45cb6 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -88,6 +88,7 @@ components: &components - homeassistant/components/persistent_notification/** - homeassistant/components/person/** - homeassistant/components/recorder/** + - homeassistant/components/repairs/** - homeassistant/components/safe_mode/** - homeassistant/components/script/** - homeassistant/components/shopping_list/** From 3920844dca5c08a4e58b124709b9e6f10cda2f31 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 14:00:35 +0200 Subject: [PATCH 588/820] Adjust repairs re-exports (#75492) --- homeassistant/components/repairs/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index 5a85d1999d0..f9a478514aa 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -6,10 +6,17 @@ from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api from .const import DOMAIN -from .issue_handler import RepairsFlow, async_create_issue, async_delete_issue +from .issue_handler import async_create_issue, async_delete_issue from .issue_registry import async_load as async_load_issue_registry +from .models import IssueSeverity, RepairsFlow -__all__ = ["DOMAIN", "RepairsFlow", "async_create_issue", "async_delete_issue"] +__all__ = [ + "async_create_issue", + "async_delete_issue", + "DOMAIN", + "IssueSeverity", + "RepairsFlow", +] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: From fb4aff25a25dcabe13b891093fe562e3488b92cf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Jul 2022 14:46:06 +0200 Subject: [PATCH 589/820] Create issues in demo integration (#75081) * Create issues in demo integration * Add unfixable non-expiring issue * Update test * Adjust tests * update translations * add hassfest translation schema * Update homeassistant/components/demo/translations/en.json Co-authored-by: Zack Barett * Rename Resolution Center -> Repairs * Update homeassistant/components/demo/strings.json Co-authored-by: Zack Barett * Adjust hassfest to require description or fix_flow * Update homeassistant/components/demo/repairs.py Co-authored-by: Martin Hjelmare * Update tests/components/demo/test_init.py Co-authored-by: Martin Hjelmare * Add missing translation strings * black * Adjust repairs imports Co-authored-by: Bram Kragten Co-authored-by: Franck Nijhof Co-authored-by: Zack Barett Co-authored-by: Martin Hjelmare --- homeassistant/components/demo/__init__.py | 34 +++++ homeassistant/components/demo/manifest.json | 2 +- homeassistant/components/demo/repairs.py | 33 +++++ homeassistant/components/demo/strings.json | 21 +++ .../components/demo/translations/en.json | 24 ++++ script/hassfest/translations.py | 23 ++- tests/components/demo/test_init.py | 133 +++++++++++++++++- 7 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/demo/repairs.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 2602d674005..3f0bb09cdbd 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -10,6 +10,7 @@ from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -177,6 +178,39 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen(EVENT_HOMEASSISTANT_START, demo_start_listener) + # Create issues + async_create_issue( + hass, + DOMAIN, + "transmogrifier_deprecated", + breaks_in_ha_version="2023.1.1", + is_fixable=False, + learn_more_url="https://en.wiktionary.org/wiki/transmogrifier", + severity=IssueSeverity.WARNING, + translation_key="transmogrifier_deprecated", + ) + + async_create_issue( + hass, + DOMAIN, + "out_of_blinker_fluid", + breaks_in_ha_version="2023.1.1", + is_fixable=True, + learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU", + severity=IssueSeverity.CRITICAL, + translation_key="out_of_blinker_fluid", + ) + + async_create_issue( + hass, + DOMAIN, + "unfixable_problem", + is_fixable=False, + learn_more_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", + severity=IssueSeverity.WARNING, + translation_key="unfixable_problem", + ) + return True diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index df6fa494079..2965a66e23e 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -3,7 +3,7 @@ "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", "after_dependencies": ["recorder"], - "dependencies": ["conversation", "group", "zone"], + "dependencies": ["conversation", "group", "repairs", "zone"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "calculated" diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/demo/repairs.py new file mode 100644 index 00000000000..e5d31c18971 --- /dev/null +++ b/homeassistant/components/demo/repairs.py @@ -0,0 +1,33 @@ +"""Repairs platform for the demo integration.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow + + +class DemoFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await (self.async_step_confirm()) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + return self.async_create_entry(title="Fixed issue", data={}) + + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +async def async_create_fix_flow(hass, issue_id): + """Create flow.""" + return DemoFixFlow() diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index c861ca9e5e9..a3a5b11f336 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -1,5 +1,26 @@ { "title": "Demo", + "issues": { + "out_of_blinker_fluid": { + "title": "The blinker fluid is empty and needs to be refilled", + "fix_flow": { + "step": { + "confirm": { + "title": "Blinker fluid needs to be refilled", + "description": "Press OK when blinker fluid has been refilled" + } + } + } + }, + "transmogrifier_deprecated": { + "title": "The transmogrifier component is deprecated", + "description": "The transmogrifier component is now deprecated due to the lack of local control available in the new API" + }, + "unfixable_problem": { + "title": "This is not a fixable problem", + "description": "This issue is never going to give up." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/en.json b/homeassistant/components/demo/translations/en.json index 2e70c88962a..326e741f764 100644 --- a/homeassistant/components/demo/translations/en.json +++ b/homeassistant/components/demo/translations/en.json @@ -1,6 +1,30 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Press OK when blinker fluid has been refilled", + "title": "Blinker fluid needs to be refilled" + } + } + }, + "title": "The blinker fluid is empty and needs to be refilled" + }, + "transmogrifier_deprecated": { + "description": "The transmogrifier component is now deprecated due to the lack of local control available in the new API", + "title": "The transmogrifier component is deprecated" + }, + "unfixable_problem": { + "description": "This issue is never going to give up.", + "title": "This is not a fixable problem" + } + }, "options": { "step": { + "init": { + "data": {} + }, "options_1": { "data": { "bool": "Optional boolean", diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index a1f520808f6..9c4f75f1b2d 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -21,7 +21,7 @@ REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" -# Only allow translatino of integration names if they contain non-brand names +# Only allow translation of integration names if they contain non-brand names ALLOW_NAME_TRANSLATION = { "cert_expiry", "cpuspeed", @@ -185,7 +185,7 @@ def gen_data_entry_schema( return vol.All(*validators) -def gen_strings_schema(config: Config, integration: Integration): +def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: """Generate a strings schema.""" return vol.Schema( { @@ -227,6 +227,25 @@ def gen_strings_schema(config: Config, integration: Integration): vol.Optional("application_credentials"): { vol.Optional("description"): cv.string_with_no_html, }, + vol.Optional("issues"): { + str: vol.All( + cv.has_at_least_one_key("description", "fix_flow"), + vol.Schema( + { + vol.Required("title"): cv.string_with_no_html, + vol.Exclusive( + "description", "fixable" + ): cv.string_with_no_html, + vol.Exclusive("fix_flow", "fixable"): gen_data_entry_schema( + config=config, + integration=integration, + flow_title=UNDEFINED, + require_step_title=False, + ), + }, + ), + ) + }, } ) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index fa0aff8223b..85ff2a16405 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,6 +1,7 @@ """The tests for the Demo component.""" +from http import HTTPStatus import json -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest @@ -69,3 +70,133 @@ async def test_demo_statistics(hass, recorder_mock): "statistic_id": "demo:energy_consumption", "unit_of_measurement": "kWh", } in statistic_ids + + +async def test_issues_created(hass, hass_client, hass_ws_client): + """Test issues are created and can be fixed.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "transmogrifier_deprecated", + "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", + "severity": "warning", + "translation_key": "transmogrifier_deprecated", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": True, + "issue_id": "out_of_blinker_fluid", + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "out_of_blinker_fluid", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "unfixable_problem", + "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "severity": "warning", + "translation_key": "unfixable_problem", + "translation_placeholders": None, + }, + ] + } + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": "demo", "issue_id": "out_of_blinker_fluid"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "data_schema": [], + "description_placeholders": None, + "errors": None, + "flow_id": ANY, + "handler": "demo", + "last_step": None, + "step_id": "confirm", + "type": "form", + } + + url = f"/api/repairs/issues/fix/{flow_id}" + resp = await client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "description": None, + "description_placeholders": None, + "flow_id": flow_id, + "handler": "demo", + "title": "Fixed issue", + "type": "create_entry", + "version": 1, + } + + await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "transmogrifier_deprecated", + "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", + "severity": "warning", + "translation_key": "transmogrifier_deprecated", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "unfixable_problem", + "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "severity": "warning", + "translation_key": "unfixable_problem", + "translation_placeholders": None, + }, + ] + } From 8ad2bed36388434b413c1805b94815edc4987759 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 20 Jul 2022 15:51:54 +0200 Subject: [PATCH 590/820] Fix Netgear update entity (#75496) --- homeassistant/components/netgear/update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/update.py b/homeassistant/components/netgear/update.py index 8d4a9b4912a..e913d488c8e 100644 --- a/homeassistant/components/netgear/update.py +++ b/homeassistant/components/netgear/update.py @@ -59,7 +59,9 @@ class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity): """Latest version available for install.""" if self.coordinator.data is not None: new_version = self.coordinator.data.get("NewVersion") - if new_version is not None: + if new_version is not None and not new_version.startswith( + self.installed_version + ): return new_version return self.installed_version From 877a4030aa1ce72b59d355a33c59f98a8506e911 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 16:29:07 +0200 Subject: [PATCH 591/820] Add repairs as frontend dependency (#75501) --- homeassistant/components/frontend/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 288b4796e0e..f19ef00e2b7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -12,6 +12,7 @@ "http", "lovelace", "onboarding", + "repairs", "search", "system_log", "websocket_api" From 87cfe215674a9eb18910f24bab3cd757d119825f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 18:55:33 +0200 Subject: [PATCH 592/820] Remove XBee integration (#75502) --- .coveragerc | 1 - homeassistant/components/xbee/__init__.py | 441 ------------------ .../components/xbee/binary_sensor.py | 29 -- homeassistant/components/xbee/const.py | 5 - homeassistant/components/xbee/light.py | 34 -- homeassistant/components/xbee/manifest.json | 10 - homeassistant/components/xbee/sensor.py | 97 ---- homeassistant/components/xbee/switch.py | 29 -- 8 files changed, 646 deletions(-) delete mode 100644 homeassistant/components/xbee/__init__.py delete mode 100644 homeassistant/components/xbee/binary_sensor.py delete mode 100644 homeassistant/components/xbee/const.py delete mode 100644 homeassistant/components/xbee/light.py delete mode 100644 homeassistant/components/xbee/manifest.json delete mode 100644 homeassistant/components/xbee/sensor.py delete mode 100644 homeassistant/components/xbee/switch.py diff --git a/.coveragerc b/.coveragerc index 3645286980b..0dd9f18af34 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1523,7 +1523,6 @@ omit = homeassistant/components/zha/light.py homeassistant/components/zha/sensor.py homeassistant/components/zhong_hong/climate.py - homeassistant/components/xbee/* homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* homeassistant/components/supla/* diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py deleted file mode 100644 index 6a7aba16b95..00000000000 --- a/homeassistant/components/xbee/__init__.py +++ /dev/null @@ -1,441 +0,0 @@ -"""Support for XBee Zigbee devices.""" -# pylint: disable=import-error -from binascii import hexlify, unhexlify -import logging - -from serial import Serial, SerialException -import voluptuous as vol -from xbee_helper import ZigBee -import xbee_helper.const as xb_const -from xbee_helper.device import convert_adc -from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure - -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - CONF_ADDRESS, - CONF_DEVICE, - CONF_NAME, - CONF_PIN, - EVENT_HOMEASSISTANT_STOP, - PERCENTAGE, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -SIGNAL_XBEE_FRAME_RECEIVED = "xbee_frame_received" - -CONF_BAUD = "baud" - -DEFAULT_DEVICE = "/dev/ttyUSB0" -DEFAULT_BAUD = 9600 -DEFAULT_ADC_MAX_VOLTS = 1.2 - -ATTR_FRAME = "frame" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string, - vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -PLATFORM_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_PIN): cv.positive_int, - vol.Optional(CONF_ADDRESS): cv.string, - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the connection to the XBee Zigbee device.""" - usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE) - baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD)) - try: - ser = Serial(usb_device, baud) - except SerialException as exc: - _LOGGER.exception("Unable to open serial port for XBee: %s", exc) - return False - zigbee_device = ZigBee(ser) - - def close_serial_port(*args): - """Close the serial port we're using to communicate with the XBee.""" - zigbee_device.zb.serial.close() - - def _frame_received(frame): - """Run when a XBee Zigbee frame is received. - - Pickles the frame, then encodes it into base64 since it contains - non JSON serializable binary. - """ - dispatcher_send(hass, SIGNAL_XBEE_FRAME_RECEIVED, frame) - - hass.data[DOMAIN] = zigbee_device - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_serial_port) - zigbee_device.add_frame_rx_handler(_frame_received) - - return True - - -def frame_is_relevant(entity, frame): - """Test whether the frame is relevant to the entity.""" - if frame.get("source_addr_long") != entity.config.address: - return False - return "samples" in frame - - -class XBeeConfig: - """Handle the fetching of configuration from the config file.""" - - def __init__(self, config): - """Initialize the configuration.""" - self._config = config - self._should_poll = config.get("poll", True) - - @property - def name(self): - """Return the name given to the entity.""" - return self._config["name"] - - @property - def address(self): - """Return the address of the device. - - If an address has been provided, unhexlify it, otherwise return None - as we're talking to our local XBee device. - """ - if (address := self._config.get("address")) is not None: - return unhexlify(address) - return address - - @property - def should_poll(self): - """Return the polling state.""" - return self._should_poll - - -class XBeePinConfig(XBeeConfig): - """Handle the fetching of configuration from the configuration file.""" - - @property - def pin(self): - """Return the GPIO pin number.""" - return self._config["pin"] - - -class XBeeDigitalInConfig(XBeePinConfig): - """A subclass of XBeePinConfig.""" - - def __init__(self, config): - """Initialise the XBee Zigbee Digital input config.""" - super().__init__(config) - self._bool2state, self._state2bool = self.boolean_maps - - @property - def boolean_maps(self): - """Create mapping dictionaries for potential inversion of booleans. - - Create dicts to map the pin state (true/false) to potentially inverted - values depending on the on_state config value which should be set to - "low" or "high". - """ - if self._config.get("on_state", "").lower() == "low": - bool2state = {True: False, False: True} - else: - bool2state = {True: True, False: False} - state2bool = {v: k for k, v in bool2state.items()} - return bool2state, state2bool - - @property - def bool2state(self): - """Return a dictionary mapping the internal value to the Zigbee value. - - For the translation of on/off as being pin high or low. - """ - return self._bool2state - - @property - def state2bool(self): - """Return a dictionary mapping the Zigbee value to the internal value. - - For the translation of pin high/low as being on or off. - """ - return self._state2bool - - -class XBeeDigitalOutConfig(XBeePinConfig): - """A subclass of XBeePinConfig. - - Set _should_poll to default as False instead of True. The value will - still be overridden by the presence of a 'poll' config entry. - """ - - def __init__(self, config): - """Initialize the XBee Zigbee Digital out.""" - super().__init__(config) - self._bool2state, self._state2bool = self.boolean_maps - self._should_poll = config.get("poll", False) - - @property - def boolean_maps(self): - """Create dicts to map booleans to pin high/low and vice versa. - - Depends on the config item "on_state" which should be set to "low" - or "high". - """ - if self._config.get("on_state", "").lower() == "low": - bool2state = { - True: xb_const.GPIO_DIGITAL_OUTPUT_LOW, - False: xb_const.GPIO_DIGITAL_OUTPUT_HIGH, - } - else: - bool2state = { - True: xb_const.GPIO_DIGITAL_OUTPUT_HIGH, - False: xb_const.GPIO_DIGITAL_OUTPUT_LOW, - } - state2bool = {v: k for k, v in bool2state.items()} - return bool2state, state2bool - - @property - def bool2state(self): - """Return a dictionary mapping booleans to GPIOSetting objects. - - For the translation of on/off as being pin high or low. - """ - return self._bool2state - - @property - def state2bool(self): - """Return a dictionary mapping GPIOSetting objects to booleans. - - For the translation of pin high/low as being on or off. - """ - return self._state2bool - - -class XBeeAnalogInConfig(XBeePinConfig): - """Representation of a XBee Zigbee GPIO pin set to analog in.""" - - @property - def max_voltage(self): - """Return the voltage for ADC to report its highest value.""" - return float(self._config.get("max_volts", DEFAULT_ADC_MAX_VOLTS)) - - -class XBeeDigitalIn(Entity): - """Representation of a GPIO pin configured as a digital input.""" - - def __init__(self, config, device): - """Initialize the device.""" - self._config = config - self._device = device - self._state = False - - async def async_added_to_hass(self): - """Register callbacks.""" - - def handle_frame(frame): - """Handle an incoming frame. - - Handle an incoming frame and update our status if it contains - information relating to this device. - """ - if not frame_is_relevant(self, frame): - return - sample = next(iter(frame["samples"])) - pin_name = xb_const.DIGITAL_PINS[self._config.pin] - if pin_name not in sample: - # Doesn't contain information about our pin - return - # Set state to the value of sample, respecting any inversion - # logic from the on_state config variable. - self._state = self._config.state2bool[ - self._config.bool2state[sample[pin_name]] - ] - self.schedule_update_ha_state() - - async_dispatcher_connect(self.hass, SIGNAL_XBEE_FRAME_RECEIVED, handle_frame) - - @property - def name(self): - """Return the name of the input.""" - return self._config.name - - @property - def config(self): - """Return the entity's configuration.""" - return self._config - - @property - def should_poll(self): - """Return the state of the polling, if needed.""" - return self._config.should_poll - - @property - def is_on(self): - """Return True if the Entity is on, else False.""" - return self._state - - def update(self): - """Ask the Zigbee device what state its input pin is in.""" - try: - sample = self._device.get_sample(self._config.address) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get sample from " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - return - except ZigBeeException as exc: - _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) - return - pin_name = xb_const.DIGITAL_PINS[self._config.pin] - if pin_name not in sample: - _LOGGER.warning( - "Pin %s (%s) was not in the sample provided by Zigbee device %s", - self._config.pin, - pin_name, - hexlify(self._config.address), - ) - return - self._state = self._config.state2bool[sample[pin_name]] - - -class XBeeDigitalOut(XBeeDigitalIn): - """Representation of a GPIO pin configured as a digital input.""" - - def _set_state(self, state): - """Initialize the XBee Zigbee digital out device.""" - try: - self._device.set_gpio_pin( - self._config.pin, self._config.bool2state[state], self._config.address - ) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to set output pin on " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - return - except ZigBeeException as exc: - _LOGGER.exception("Unable to set digital pin on XBee device: %s", exc) - return - self._state = state - if not self.should_poll: - self.schedule_update_ha_state() - - def turn_on(self, **kwargs): - """Set the digital output to its 'on' state.""" - self._set_state(True) - - def turn_off(self, **kwargs): - """Set the digital output to its 'off' state.""" - self._set_state(False) - - def update(self): - """Ask the XBee device what its output is set to.""" - try: - pin_state = self._device.get_gpio_pin( - self._config.pin, self._config.address - ) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get output pin status" - " from Zigbee device at address: %s", - hexlify(self._config.address), - ) - return - except ZigBeeException as exc: - _LOGGER.exception( - "Unable to get output pin status from XBee device: %s", exc - ) - return - self._state = self._config.state2bool[pin_state] - - -class XBeeAnalogIn(SensorEntity): - """Representation of a GPIO pin configured as an analog input.""" - - _attr_native_unit_of_measurement = PERCENTAGE - - def __init__(self, config, device): - """Initialize the XBee analog in device.""" - self._config = config - self._device = device - self._value = None - - async def async_added_to_hass(self): - """Register callbacks.""" - - def handle_frame(frame): - """Handle an incoming frame. - - Handle an incoming frame and update our status if it contains - information relating to this device. - """ - if not frame_is_relevant(self, frame): - return - sample = frame["samples"].pop() - pin_name = xb_const.ANALOG_PINS[self._config.pin] - if pin_name not in sample: - # Doesn't contain information about our pin - return - self._value = convert_adc( - sample[pin_name], xb_const.ADC_PERCENTAGE, self._config.max_voltage - ) - self.schedule_update_ha_state() - - async_dispatcher_connect(self.hass, SIGNAL_XBEE_FRAME_RECEIVED, handle_frame) - - @property - def name(self): - """Return the name of the input.""" - return self._config.name - - @property - def config(self): - """Return the entity's configuration.""" - return self._config - - @property - def should_poll(self): - """Return the polling state, if needed.""" - return self._config.should_poll - - @property - def sensor_state(self): - """Return the state of the entity.""" - return self._value - - def update(self): - """Get the latest reading from the ADC.""" - try: - self._value = self._device.read_analog_pin( - self._config.pin, - self._config.max_voltage, - self._config.address, - xb_const.ADC_PERCENTAGE, - ) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get sample from " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - except ZigBeeException as exc: - _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) diff --git a/homeassistant/components/xbee/binary_sensor.py b/homeassistant/components/xbee/binary_sensor.py deleted file mode 100644 index b1639085993..00000000000 --- a/homeassistant/components/xbee/binary_sensor.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Support for Zigbee binary sensors.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import PLATFORM_SCHEMA, XBeeDigitalIn, XBeeDigitalInConfig -from .const import CONF_ON_STATE, DOMAIN, STATES - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)}) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the XBee Zigbee binary sensor platform.""" - zigbee_device = hass.data[DOMAIN] - add_entities([XBeeBinarySensor(XBeeDigitalInConfig(config), zigbee_device)], True) - - -class XBeeBinarySensor(XBeeDigitalIn, BinarySensorEntity): - """Use XBeeDigitalIn as binary sensor.""" diff --git a/homeassistant/components/xbee/const.py b/homeassistant/components/xbee/const.py deleted file mode 100644 index a77e71e92f5..00000000000 --- a/homeassistant/components/xbee/const.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Constants for the xbee integration.""" -CONF_ON_STATE = "on_state" -DEFAULT_ON_STATE = "high" -DOMAIN = "xbee" -STATES = ["high", "low"] diff --git a/homeassistant/components/xbee/light.py b/homeassistant/components/xbee/light.py deleted file mode 100644 index 126eaf91f9d..00000000000 --- a/homeassistant/components/xbee/light.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Support for XBee Zigbee lights.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.light import ColorMode, LightEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig -from .const import CONF_ON_STATE, DEFAULT_ON_STATE, DOMAIN, STATES - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_ON_STATE, default=DEFAULT_ON_STATE): vol.In(STATES)} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Create and add an entity based on the configuration.""" - zigbee_device = hass.data[DOMAIN] - add_entities([XBeeLight(XBeeDigitalOutConfig(config), zigbee_device)]) - - -class XBeeLight(XBeeDigitalOut, LightEntity): - """Use XBeeDigitalOut as light.""" - - _attr_color_mode = ColorMode.ONOFF - _attr_supported_color_modes = {ColorMode.ONOFF} diff --git a/homeassistant/components/xbee/manifest.json b/homeassistant/components/xbee/manifest.json deleted file mode 100644 index 150036129d2..00000000000 --- a/homeassistant/components/xbee/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "disabled": "Integration library not compatible with Python 3.10", - "domain": "xbee", - "name": "XBee", - "documentation": "https://www.home-assistant.io/integrations/xbee", - "requirements": ["xbee-helper==0.0.7"], - "codeowners": [], - "iot_class": "local_polling", - "loggers": ["xbee_helper"] -} diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py deleted file mode 100644 index 9cea60ade8c..00000000000 --- a/homeassistant/components/xbee/sensor.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Support for XBee Zigbee sensors.""" -# pylint: disable=import-error -from __future__ import annotations - -from binascii import hexlify -import logging - -import voluptuous as vol -from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.const import CONF_TYPE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import DOMAIN, PLATFORM_SCHEMA, XBeeAnalogIn, XBeeAnalogInConfig, XBeeConfig - -_LOGGER = logging.getLogger(__name__) - -CONF_MAX_VOLTS = "max_volts" - -DEFAULT_VOLTS = 1.2 -TYPES = ["analog", "temperature"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In(TYPES), - vol.Optional(CONF_MAX_VOLTS, default=DEFAULT_VOLTS): vol.Coerce(float), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the XBee Zigbee platform. - - Uses the 'type' config value to work out which type of Zigbee sensor we're - dealing with and instantiates the relevant classes to handle it. - """ - zigbee_device = hass.data[DOMAIN] - typ = config[CONF_TYPE] - - try: - sensor_class, config_class = TYPE_CLASSES[typ] - except KeyError: - _LOGGER.exception("Unknown XBee Zigbee sensor type: %s", typ) - return - - add_entities([sensor_class(config_class(config), zigbee_device)], True) - - -class XBeeTemperatureSensor(SensorEntity): - """Representation of XBee Pro temperature sensor.""" - - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = TEMP_CELSIUS - - def __init__(self, config, device): - """Initialize the sensor.""" - self._config = config - self._device = device - self._temp = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._config.name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._temp - - def update(self): - """Get the latest data.""" - try: - self._temp = self._device.get_temperature(self._config.address) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get sample from " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - except ZigBeeException as exc: - _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) - - -# This must be below the classes to which it refers. -TYPE_CLASSES = { - "temperature": (XBeeTemperatureSensor, XBeeConfig), - "analog": (XBeeAnalogIn, XBeeAnalogInConfig), -} diff --git a/homeassistant/components/xbee/switch.py b/homeassistant/components/xbee/switch.py deleted file mode 100644 index 9cc25fbf7d2..00000000000 --- a/homeassistant/components/xbee/switch.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Support for XBee Zigbee switches.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig -from .const import CONF_ON_STATE, DOMAIN, STATES - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)}) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the XBee Zigbee switch platform.""" - zigbee_device = hass.data[DOMAIN] - add_entities([XBeeSwitch(XBeeDigitalOutConfig(config), zigbee_device)]) - - -class XBeeSwitch(XBeeDigitalOut, SwitchEntity): - """Representation of a XBee Zigbee Digital Out device.""" From 518001f00b92d24a699e9039e5c066f7c9795fb1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 18:58:48 +0200 Subject: [PATCH 593/820] Remove SoChain integration (#75505) --- .coveragerc | 1 - homeassistant/components/sochain/__init__.py | 1 - .../components/sochain/manifest.json | 10 --- homeassistant/components/sochain/sensor.py | 88 ------------------- 4 files changed, 100 deletions(-) delete mode 100644 homeassistant/components/sochain/__init__.py delete mode 100644 homeassistant/components/sochain/manifest.json delete mode 100644 homeassistant/components/sochain/sensor.py diff --git a/.coveragerc b/.coveragerc index 0dd9f18af34..f266e557e59 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1110,7 +1110,6 @@ omit = homeassistant/components/smtp/notify.py homeassistant/components/snapcast/* homeassistant/components/snmp/* - homeassistant/components/sochain/sensor.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge/sensor.py diff --git a/homeassistant/components/sochain/__init__.py b/homeassistant/components/sochain/__init__.py deleted file mode 100644 index 31a000d3947..00000000000 --- a/homeassistant/components/sochain/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sochain component.""" diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json deleted file mode 100644 index 5a568340197..00000000000 --- a/homeassistant/components/sochain/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "disabled": "Integration library depends on async_timeout==3.x.x", - "domain": "sochain", - "name": "SoChain", - "documentation": "https://www.home-assistant.io/integrations/sochain", - "requirements": ["python-sochain-api==0.0.2"], - "codeowners": [], - "iot_class": "cloud_polling", - "loggers": ["pysochain"] -} diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py deleted file mode 100644 index 157d94b8706..00000000000 --- a/homeassistant/components/sochain/sensor.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Support for watching multiple cryptocurrencies.""" -# pylint: disable=import-error -from __future__ import annotations - -from datetime import timedelta - -from pysochain import ChainSo -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -ATTRIBUTION = "Data provided by chain.so" - -CONF_NETWORK = "network" - -DEFAULT_NAME = "Crypto Balance" - -SCAN_INTERVAL = timedelta(minutes=5) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Required(CONF_NETWORK): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the sochain sensors.""" - - address = config[CONF_ADDRESS] - network = config[CONF_NETWORK] - name = config[CONF_NAME] - - session = async_get_clientsession(hass) - chainso = ChainSo(network, address, hass.loop, session) - - async_add_entities([SochainSensor(name, network.upper(), chainso)], True) - - -class SochainSensor(SensorEntity): - """Representation of a Sochain sensor.""" - - def __init__(self, name, unit_of_measurement, chainso): - """Initialize the sensor.""" - self._name = name - self._unit_of_measurement = unit_of_measurement - self.chainso = chainso - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return ( - self.chainso.data.get("confirmed_balance") - if self.chainso is not None - else None - ) - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - async def async_update(self): - """Get the latest state of the sensor.""" - await self.chainso.async_get_data() From 4395b967f2b06de7fc62a1be2853fd2465c59886 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 19:15:55 +0200 Subject: [PATCH 594/820] Remove Google Play Music Desktop Player (GPMDP) integration (#75508) --- .coveragerc | 1 - homeassistant/components/gpmdp/__init__.py | 1 - homeassistant/components/gpmdp/manifest.json | 10 - .../components/gpmdp/media_player.py | 382 ------------------ 4 files changed, 394 deletions(-) delete mode 100644 homeassistant/components/gpmdp/__init__.py delete mode 100644 homeassistant/components/gpmdp/manifest.json delete mode 100644 homeassistant/components/gpmdp/media_player.py diff --git a/.coveragerc b/.coveragerc index f266e557e59..873629ddd0b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -442,7 +442,6 @@ omit = homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_pubsub/__init__.py - homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py diff --git a/homeassistant/components/gpmdp/__init__.py b/homeassistant/components/gpmdp/__init__.py deleted file mode 100644 index a8aa82c69c3..00000000000 --- a/homeassistant/components/gpmdp/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The gpmdp component.""" diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json deleted file mode 100644 index 51fad8e9e71..00000000000 --- a/homeassistant/components/gpmdp/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "gpmdp", - "name": "Google Play Music Desktop Player (GPMDP)", - "disabled": "Integration has incompatible requirements.", - "documentation": "https://www.home-assistant.io/integrations/gpmdp", - "requirements": ["websocket-client==0.54.0"], - "dependencies": ["configurator"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py deleted file mode 100644 index b2861e4d96d..00000000000 --- a/homeassistant/components/gpmdp/media_player.py +++ /dev/null @@ -1,382 +0,0 @@ -"""Support for Google Play Music Desktop Player.""" -from __future__ import annotations - -import json -import logging -import socket -import time -from typing import Any - -import voluptuous as vol -from websocket import _exceptions, create_connection - -from homeassistant.components import configurator -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, - MediaPlayerEntity, - MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.json import load_json, save_json - -_CONFIGURING: dict[str, Any] = {} -_LOGGER = logging.getLogger(__name__) - -DEFAULT_HOST = "localhost" -DEFAULT_NAME = "GPM Desktop Player" -DEFAULT_PORT = 5672 - -GPMDP_CONFIG_FILE = "gpmpd.conf" - -PLAYBACK_DICT = {"0": STATE_PAUSED, "1": STATE_PAUSED, "2": STATE_PLAYING} # Stopped - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } -) - - -def request_configuration(hass, config, url, add_entities_callback): - """Request configuration steps from the user.""" - if "gpmdp" in _CONFIGURING: - configurator.notify_errors( - hass, _CONFIGURING["gpmdp"], "Failed to register, please try again." - ) - - return - websocket = create_connection((url), timeout=1) - websocket.send( - json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant"], - } - ) - ) - - def gpmdp_configuration_callback(callback_data): - """Handle configuration changes.""" - while True: - - try: - msg = json.loads(websocket.recv()) - except _exceptions.WebSocketConnectionClosedException: - continue - if msg["channel"] != "connect": - continue - if msg["payload"] != "CODE_REQUIRED": - continue - pin = callback_data.get("pin") - websocket.send( - json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant", pin], - } - ) - ) - tmpmsg = json.loads(websocket.recv()) - if tmpmsg["channel"] == "time": - _LOGGER.error( - "Error setting up GPMDP. Please pause " - "the desktop player and try again" - ) - break - if (code := tmpmsg["payload"]) == "CODE_REQUIRED": - continue - setup_gpmdp(hass, config, code, add_entities_callback) - save_json(hass.config.path(GPMDP_CONFIG_FILE), {"CODE": code}) - websocket.send( - json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant", code], - } - ) - ) - websocket.close() - break - - _CONFIGURING["gpmdp"] = configurator.request_config( - DEFAULT_NAME, - gpmdp_configuration_callback, - description=( - "Enter the pin that is displayed in the " - "Google Play Music Desktop Player." - ), - submit_caption="Submit", - fields=[{"id": "pin", "name": "Pin Code", "type": "number"}], - ) - - -def setup_gpmdp(hass, config, code, add_entities): - """Set up gpmdp.""" - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - url = f"ws://{host}:{port}" - - if not code: - request_configuration(hass, config, url, add_entities) - return - - if "gpmdp" in _CONFIGURING: - configurator.request_done(hass, _CONFIGURING.pop("gpmdp")) - - add_entities([GPMDP(name, url, code)], True) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the GPMDP platform.""" - codeconfig = load_json(hass.config.path(GPMDP_CONFIG_FILE)) - if codeconfig: - code = codeconfig.get("CODE") if isinstance(codeconfig, dict) else None - elif discovery_info is not None: - if "gpmdp" in _CONFIGURING: - return - code = None - else: - code = None - setup_gpmdp(hass, config, code, add_entities) - - -class GPMDP(MediaPlayerEntity): - """Representation of a GPMDP.""" - - _attr_supported_features = ( - MediaPlayerEntityFeature.PAUSE - | MediaPlayerEntityFeature.PREVIOUS_TRACK - | MediaPlayerEntityFeature.NEXT_TRACK - | MediaPlayerEntityFeature.SEEK - | MediaPlayerEntityFeature.VOLUME_SET - | MediaPlayerEntityFeature.PLAY - ) - - def __init__(self, name, url, code): - """Initialize the media player.""" - - self._connection = create_connection - self._url = url - self._authorization_code = code - self._name = name - self._status = STATE_OFF - self._ws = None - self._title = None - self._artist = None - self._albumart = None - self._seek_position = None - self._duration = None - self._volume = None - self._request_id = 0 - self._available = True - - def get_ws(self): - """Check if the websocket is setup and connected.""" - if self._ws is None: - try: - self._ws = self._connection((self._url), timeout=1) - msg = json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant", self._authorization_code], - } - ) - self._ws.send(msg) - except (socket.timeout, ConnectionRefusedError, ConnectionResetError): - self._ws = None - return self._ws - - def send_gpmdp_msg(self, namespace, method, with_id=True): - """Send ws messages to GPMDP and verify request id in response.""" - - try: - if (websocket := self.get_ws()) is None: - self._status = STATE_OFF - return - self._request_id += 1 - websocket.send( - json.dumps( - { - "namespace": namespace, - "method": method, - "requestID": self._request_id, - } - ) - ) - if not with_id: - return - while True: - msg = json.loads(websocket.recv()) - if "requestID" in msg and msg["requestID"] == self._request_id: - return msg - except ( - ConnectionRefusedError, - ConnectionResetError, - _exceptions.WebSocketTimeoutException, - _exceptions.WebSocketProtocolException, - _exceptions.WebSocketPayloadException, - _exceptions.WebSocketConnectionClosedException, - ): - self._ws = None - - def update(self): - """Get the latest details from the player.""" - time.sleep(1) - try: - self._available = True - playstate = self.send_gpmdp_msg("playback", "getPlaybackState") - if playstate is None: - return - self._status = PLAYBACK_DICT[str(playstate["value"])] - time_data = self.send_gpmdp_msg("playback", "getCurrentTime") - if time_data is not None: - self._seek_position = int(time_data["value"] / 1000) - track_data = self.send_gpmdp_msg("playback", "getCurrentTrack") - if track_data is not None: - self._title = track_data["value"]["title"] - self._artist = track_data["value"]["artist"] - self._albumart = track_data["value"]["albumArt"] - self._duration = int(track_data["value"]["duration"] / 1000) - volume_data = self.send_gpmdp_msg("volume", "getVolume") - if volume_data is not None: - self._volume = volume_data["value"] / 100 - except OSError: - self._available = False - - @property - def available(self): - """Return if media player is available.""" - return self._available - - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - def state(self): - """Return the state of the device.""" - return self._status - - @property - def media_title(self): - """Title of current playing media.""" - return self._title - - @property - def media_artist(self): - """Artist of current playing media (Music track only).""" - return self._artist - - @property - def media_image_url(self): - """Image url of current playing media.""" - return self._albumart - - @property - def media_seek_position(self): - """Time in seconds of current seek position.""" - return self._seek_position - - @property - def media_duration(self): - """Time in seconds of current song duration.""" - return self._duration - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def name(self): - """Return the name of the device.""" - return self._name - - def media_next_track(self): - """Send media_next command to media player.""" - self.send_gpmdp_msg("playback", "forward", False) - - def media_previous_track(self): - """Send media_previous command to media player.""" - self.send_gpmdp_msg("playback", "rewind", False) - - def media_play(self): - """Send media_play command to media player.""" - self.send_gpmdp_msg("playback", "playPause", False) - self._status = STATE_PLAYING - self.schedule_update_ha_state() - - def media_pause(self): - """Send media_pause command to media player.""" - self.send_gpmdp_msg("playback", "playPause", False) - self._status = STATE_PAUSED - self.schedule_update_ha_state() - - def media_seek(self, position): - """Send media_seek command to media player.""" - if (websocket := self.get_ws()) is None: - return - websocket.send( - json.dumps( - { - "namespace": "playback", - "method": "setCurrentTime", - "arguments": [position * 1000], - } - ) - ) - self.schedule_update_ha_state() - - def volume_up(self): - """Send volume_up command to media player.""" - if (websocket := self.get_ws()) is None: - return - websocket.send('{"namespace": "volume", "method": "increaseVolume"}') - self.schedule_update_ha_state() - - def volume_down(self): - """Send volume_down command to media player.""" - if (websocket := self.get_ws()) is None: - return - websocket.send('{"namespace": "volume", "method": "decreaseVolume"}') - self.schedule_update_ha_state() - - def set_volume_level(self, volume): - """Set volume on media player, range(0..1).""" - if (websocket := self.get_ws()) is None: - return - websocket.send( - json.dumps( - { - "namespace": "volume", - "method": "setVolume", - "arguments": [volume * 100], - } - ) - ) - self.schedule_update_ha_state() From 48e82ff62fc6719fdc4f5fec8db52b26c7d82937 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Jul 2022 12:25:17 -0500 Subject: [PATCH 595/820] Fix failure to raise on bad YAML syntax from include files (#75510) Co-authored-by: Franck Nijhof --- homeassistant/util/yaml/loader.py | 4 ++-- tests/util/yaml/fixtures/bad.yaml.txt | 26 ++++++++++++++++++++++++++ tests/util/yaml/test_init.py | 10 ++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 tests/util/yaml/fixtures/bad.yaml.txt diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index e3add3a7c44..09e19af6840 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Iterator import fnmatch -from io import StringIO +from io import StringIO, TextIOWrapper import logging import os from pathlib import Path @@ -169,7 +169,7 @@ def parse_yaml( except yaml.YAMLError: # Loading failed, so we now load with the slow line loader # since the C one will not give us line numbers - if isinstance(content, (StringIO, TextIO)): + if isinstance(content, (StringIO, TextIO, TextIOWrapper)): # Rewind the stream so we can try again content.seek(0, 0) return _parse_yaml_pure_python(content, secrets) diff --git a/tests/util/yaml/fixtures/bad.yaml.txt b/tests/util/yaml/fixtures/bad.yaml.txt new file mode 100644 index 00000000000..8f6a62a6511 --- /dev/null +++ b/tests/util/yaml/fixtures/bad.yaml.txt @@ -0,0 +1,26 @@ +- id: '1658085239190' + alias: Config validation test + description: '' + trigger: + - platform: time + at: 00:02:03 + condition: [] + action: + - service: script.notify_admin + data: + title: 'Here's something that does not work...!' + message: failing + mode: single +- id: '165808523911590' + alias: Config validation test FIXED + description: '' + trigger: + - platform: time + at: 00:02:03 + condition: [] + action: + - service: script.notify_admin + data: + title: 'Here is something that should work...!' + message: fixed? + mode: single diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 11dc40233dc..8d1b7c1adf1 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -2,6 +2,7 @@ import importlib import io import os +import pathlib import unittest from unittest.mock import patch @@ -490,3 +491,12 @@ def test_input(try_both_loaders, try_both_dumpers): def test_c_loader_is_available_in_ci(): """Verify we are testing the C loader in the CI.""" assert yaml.loader.HAS_C_LOADER is True + + +async def test_loading_actual_file_with_syntax(hass, try_both_loaders): + """Test loading a real file with syntax errors.""" + with pytest.raises(HomeAssistantError): + fixture_path = pathlib.Path(__file__).parent.joinpath( + "fixtures", "bad.yaml.txt" + ) + await hass.async_add_executor_job(load_yaml_config_file, fixture_path) From 7ba3227d52e12c3f90b8488335a2a55757b90a13 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 20 Jul 2022 19:27:11 +0200 Subject: [PATCH 596/820] Fix - Forcast.solar issue on saving settings in options flow without api key (#75504) --- homeassistant/components/forecast_solar/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py index 86de17ef285..600ed363a8b 100644 --- a/homeassistant/components/forecast_solar/config_flow.py +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -99,7 +99,7 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): CONF_API_KEY, description={ "suggested_value": self.config_entry.options.get( - CONF_API_KEY + CONF_API_KEY, "" ) }, ): str, From a91ca46342789861e310ac04e189430cc373a748 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Jul 2022 11:29:23 -0600 Subject: [PATCH 597/820] Fix incorrect Ambient PWS lightning strike sensor state classes (#75520) --- homeassistant/components/ambient_station/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 2944f33938d..4eae53b6a03 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -281,14 +281,14 @@ SENSOR_DESCRIPTIONS = ( name="Lightning strikes per day", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_HOUR, name="Lightning strikes per hour", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, From 079460d2dd09562577b98d5371a17f74eaac7b8a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 20 Jul 2022 22:11:05 +0300 Subject: [PATCH 598/820] Bump aioshelly to 2.0.1 (#75523) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index a9aade00933..48e48b618e4 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==2.0.0"], + "requirements": ["aioshelly==2.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index b3395085f2e..6e525274af2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==2.0.0 +aioshelly==2.0.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f53ea0fd4cf..cf07a07117e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==2.0.0 +aioshelly==2.0.1 # homeassistant.components.skybell aioskybell==22.7.0 From b749622c0131b22c41617444c3a8b60bde8eca3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 20 Jul 2022 22:28:53 +0300 Subject: [PATCH 599/820] Migrate Huawei LTE to new entity naming style (#75303) --- .../components/huawei_lte/__init__.py | 10 +------ .../components/huawei_lte/binary_sensor.py | 30 +++++++------------ .../components/huawei_lte/device_tracker.py | 3 +- homeassistant/components/huawei_lte/sensor.py | 8 ++--- homeassistant/components/huawei_lte/switch.py | 12 +++----- tests/components/huawei_lte/test_switches.py | 2 +- 6 files changed, 22 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 3ca3daff523..bace633f128 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -586,10 +586,7 @@ class HuaweiLteBaseEntity(Entity): _available: bool = field(default=True, init=False) _unsub_handlers: list[Callable] = field(default_factory=list, init=False) - - @property - def _entity_name(self) -> str: - raise NotImplementedError + _attr_has_entity_name: bool = field(default=True, init=False) @property def _device_unique_id(self) -> str: @@ -601,11 +598,6 @@ class HuaweiLteBaseEntity(Entity): """Return unique ID for entity.""" return f"{self.router.config_entry.unique_id}-{self._device_unique_id}" - @property - def name(self) -> str: - """Return entity name.""" - return f"Huawei {self.router.device_name} {self._entity_name}" - @property def available(self) -> bool: """Return whether the entity is available.""" diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 9bd455d9d59..695b7e2e815 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -105,15 +105,13 @@ CONNECTION_STATE_ATTRIBUTES = { class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): """Huawei LTE mobile connection binary sensor.""" + _attr_name: str = field(default="Mobile connection", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_STATUS self.item = "ConnectionStatus" - @property - def _entity_name(self) -> str: - return "Mobile connection" - @property def is_on(self) -> bool: """Return whether the binary sensor is on.""" @@ -176,57 +174,49 @@ class HuaweiLteBaseWifiStatusBinarySensor(HuaweiLteBaseBinarySensor): class HuaweiLteWifiStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE WiFi status binary sensor.""" + _attr_name: str = field(default="WiFi status", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_STATUS self.item = "WifiStatus" - @property - def _entity_name(self) -> str: - return "WiFi status" - @dataclass class HuaweiLteWifi24ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE 2.4GHz WiFi status binary sensor.""" + _attr_name: str = field(default="2.4GHz WiFi status", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_FEATURE_SWITCH self.item = "wifi24g_switch_enable" - @property - def _entity_name(self) -> str: - return "2.4GHz WiFi status" - @dataclass class HuaweiLteWifi5ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE 5GHz WiFi status binary sensor.""" + _attr_name: str = field(default="5GHz WiFi status", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_FEATURE_SWITCH self.item = "wifi5g_enabled" - @property - def _entity_name(self) -> str: - return "5GHz WiFi status" - @dataclass class HuaweiLteSmsStorageFullBinarySensor(HuaweiLteBaseBinarySensor): """Huawei LTE SMS storage full binary sensor.""" + _attr_name: str = field(default="SMS storage full", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_CHECK_NOTIFICATIONS self.item = "SmsStorageFull" - @property - def _entity_name(self) -> str: - return "SMS storage full" - @property def is_on(self) -> bool: """Return whether the binary sensor is on.""" diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index ee643c59b12..179587d72d7 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -185,7 +185,8 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): _extra_state_attributes: dict[str, Any] = field(default_factory=dict, init=False) @property - def _entity_name(self) -> str: + def name(self) -> str: + """Return the name of the entity.""" return self.hostname or self.mac_address @property diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 642d8368207..44d03677ebd 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -548,6 +548,10 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): _state: StateType = field(default=STATE_UNKNOWN, init=False) _unit: str | None = field(default=None, init=False) + def __post_init__(self) -> None: + """Initialize remaining attributes.""" + self._attr_name = self.meta.name or self.item + async def async_added_to_hass(self) -> None: """Subscribe to needed data on add.""" await super().async_added_to_hass() @@ -558,10 +562,6 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): await super().async_will_remove_from_hass() self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}") - @property - def _entity_name(self) -> str: - return self.meta.name or self.item - @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 611f7212d9a..78579d62698 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -92,15 +92,13 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntityWithDevice, SwitchEntity): class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): """Huawei LTE mobile data switch device.""" + _attr_name: str = field(default="Mobile data", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_DIALUP_MOBILE_DATASWITCH self.item = "dataswitch" - @property - def _entity_name(self) -> str: - return "Mobile data" - @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" @@ -126,15 +124,13 @@ class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): class HuaweiLteWifiGuestNetworkSwitch(HuaweiLteBaseSwitch): """Huawei LTE WiFi guest network switch device.""" + _attr_name: str = field(default="WiFi guest network", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH self.item = "WifiEnable" - @property - def _entity_name(self) -> str: - return "WiFi guest network" - @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" diff --git a/tests/components/huawei_lte/test_switches.py b/tests/components/huawei_lte/test_switches.py index c5006add923..5bafed27e70 100644 --- a/tests/components/huawei_lte/test_switches.py +++ b/tests/components/huawei_lte/test_switches.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry from tests.common import MockConfigEntry -SWITCH_WIFI_GUEST_NETWORK = "switch.huawei_lte_wifi_guest_network" +SWITCH_WIFI_GUEST_NETWORK = "switch.lte_wifi_guest_network" @fixture From 01c105b89ceacbd6a3dec0613605aa463c8f4692 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 20 Jul 2022 22:34:49 +0200 Subject: [PATCH 600/820] Use `DeviceInfo.hw_version` in DenonAVR integration (#75300) --- homeassistant/components/denonavr/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 10c3cb4f6b0..16814b72bc7 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -226,9 +226,10 @@ class DenonDevice(MediaPlayerEntity): assert config_entry.unique_id self._attr_device_info = DeviceInfo( configuration_url=f"http://{config_entry.data[CONF_HOST]}/", + hw_version=config_entry.data[CONF_TYPE], identifiers={(DOMAIN, config_entry.unique_id)}, manufacturer=config_entry.data[CONF_MANUFACTURER], - model=f"{config_entry.data[CONF_MODEL]}-{config_entry.data[CONF_TYPE]}", + model=config_entry.data[CONF_MODEL], name=config_entry.title, ) self._attr_sound_mode_list = receiver.sound_mode_list From 6da25c733e014624564140a761af5201e3bf0adc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Jul 2022 15:54:37 -0500 Subject: [PATCH 601/820] Add coordinator and entity for passive bluetooth devices (#75468) Co-authored-by: Martin Hjelmare --- .../bluetooth/passive_update_coordinator.py | 341 +++++++ .../test_passive_update_coordinator.py | 868 ++++++++++++++++++ 2 files changed, 1209 insertions(+) create mode 100644 homeassistant/components/bluetooth/passive_update_coordinator.py create mode 100644 tests/components/bluetooth/test_passive_update_coordinator.py diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py new file mode 100644 index 00000000000..df4233452df --- /dev/null +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -0,0 +1,341 @@ +"""The Bluetooth integration.""" +from __future__ import annotations + +from collections.abc import Callable +import dataclasses +from datetime import datetime +import logging +import time +from typing import Any, Generic, TypeVar + +from home_assistant_bluetooth import BluetoothServiceInfo + +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later + +from . import ( + BluetoothCallbackMatcher, + BluetoothChange, + async_address_present, + async_register_callback, +) +from .const import DOMAIN + +UNAVAILABLE_SECONDS = 60 * 5 +NEVER_TIME = -UNAVAILABLE_SECONDS + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothEntityKey: + """Key for a passive bluetooth entity. + + Example: + key: temperature + device_id: outdoor_sensor_1 + """ + + key: str + device_id: str | None + + +_T = TypeVar("_T") + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothDataUpdate(Generic[_T]): + """Generic bluetooth data.""" + + devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) + entity_descriptions: dict[ + PassiveBluetoothEntityKey, EntityDescription + ] = dataclasses.field(default_factory=dict) + entity_data: dict[PassiveBluetoothEntityKey, _T] = dataclasses.field( + default_factory=dict + ) + + +_PassiveBluetoothDataUpdateCoordinatorT = TypeVar( + "_PassiveBluetoothDataUpdateCoordinatorT", + bound="PassiveBluetoothDataUpdateCoordinator[Any]", +) + + +class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): + """Passive bluetooth data update coordinator for bluetooth advertisements. + + The coordinator is responsible for keeping track of the bluetooth data, + updating subscribers, and device availability. + + The update_method must return a PassiveBluetoothDataUpdate object. Callers + are responsible for formatting the data returned from their parser into + the appropriate format. + + The coordinator will call the update_method every time the bluetooth device + receives a new advertisement with the following signature: + + update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + + As the size of each advertisement is limited, the update_method should + return a PassiveBluetoothDataUpdate object that contains only data that + should be updated. The coordinator will then dispatch subscribers based + on the data in the PassiveBluetoothDataUpdate object. The accumulated data + is available in the devices, entity_data, and entity_descriptions attributes. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + address: str, + update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + ) -> None: + """Initialize the coordinator.""" + self.hass = hass + self.logger = logger + self.name: str | None = None + self.address = address + self._listeners: list[ + Callable[[PassiveBluetoothDataUpdate[_T] | None], None] + ] = [] + self._entity_key_listeners: dict[ + PassiveBluetoothEntityKey, + list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], + ] = {} + self.update_method = update_method + + self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} + self.entity_descriptions: dict[ + PassiveBluetoothEntityKey, EntityDescription + ] = {} + self.devices: dict[str | None, DeviceInfo] = {} + + self.last_update_success = True + self._last_callback_time: float = NEVER_TIME + self._cancel_track_available: CALLBACK_TYPE | None = None + self._present = False + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._present and self.last_update_success + + @callback + def _async_cancel_available_tracker(self) -> None: + """Reset the available tracker.""" + if self._cancel_track_available: + self._cancel_track_available() + self._cancel_track_available = None + + @callback + def _async_schedule_available_tracker(self, time_remaining: float) -> None: + """Schedule the available tracker.""" + self._cancel_track_available = async_call_later( + self.hass, time_remaining, self._async_check_device_present + ) + + @callback + def _async_check_device_present(self, _: datetime) -> None: + """Check if the device is present.""" + time_passed_since_seen = time.monotonic() - self._last_callback_time + self._async_cancel_available_tracker() + if ( + not self._present + or time_passed_since_seen < UNAVAILABLE_SECONDS + or async_address_present(self.hass, self.address) + ): + self._async_schedule_available_tracker( + UNAVAILABLE_SECONDS - time_passed_since_seen + ) + return + self._present = False + self.async_update_listeners(None) + + @callback + def async_setup(self) -> CALLBACK_TYPE: + """Start the callback.""" + return async_register_callback( + self.hass, + self._async_handle_bluetooth_event, + BluetoothCallbackMatcher(address=self.address), + ) + + @callback + def async_add_entities_listener( + self, + entity_class: type[PassiveBluetoothCoordinatorEntity], + async_add_entites: AddEntitiesCallback, + ) -> Callable[[], None]: + """Add a listener for new entities.""" + created: set[PassiveBluetoothEntityKey] = set() + + @callback + def _async_add_or_update_entities( + data: PassiveBluetoothDataUpdate[_T] | None, + ) -> None: + """Listen for new entities.""" + if data is None: + return + entities: list[PassiveBluetoothCoordinatorEntity] = [] + for entity_key, description in data.entity_descriptions.items(): + if entity_key not in created: + entities.append(entity_class(self, entity_key, description)) + created.add(entity_key) + if entities: + async_add_entites(entities) + + return self.async_add_listener(_async_add_or_update_entities) + + @callback + def async_add_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + ) -> Callable[[], None]: + """Listen for all updates.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.remove(update_callback) + + self._listeners.append(update_callback) + return remove_listener + + @callback + def async_add_entity_key_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + entity_key: PassiveBluetoothEntityKey, + ) -> Callable[[], None]: + """Listen for updates by device key.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._entity_key_listeners[entity_key].remove(update_callback) + if not self._entity_key_listeners[entity_key]: + del self._entity_key_listeners[entity_key] + + self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) + return remove_listener + + @callback + def async_update_listeners( + self, data: PassiveBluetoothDataUpdate[_T] | None + ) -> None: + """Update all registered listeners.""" + # Dispatch to listeners without a filter key + for update_callback in self._listeners: + update_callback(data) + + # Dispatch to listeners with a filter key + for listeners in self._entity_key_listeners.values(): + for update_callback in listeners: + update_callback(data) + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.name = service_info.name + self._last_callback_time = time.monotonic() + self._present = True + if not self._cancel_track_available: + self._async_schedule_available_tracker(UNAVAILABLE_SECONDS) + if self.hass.is_stopping: + return + + try: + new_data = self.update_method(service_info) + except Exception as err: # pylint: disable=broad-except + self.last_update_success = False + self.logger.exception( + "Unexpected error updating %s data: %s", self.name, err + ) + return + + if not isinstance(new_data, PassiveBluetoothDataUpdate): + self.last_update_success = False # type: ignore[unreachable] + self.logger.error( + "The update_method for %s returned %s instead of a PassiveBluetoothDataUpdate", + self.name, + new_data, + ) + return + + if not self.last_update_success: + self.last_update_success = True + self.logger.info("Processing %s data recovered", self.name) + + self.devices.update(new_data.devices) + self.entity_descriptions.update(new_data.entity_descriptions) + self.entity_data.update(new_data.entity_data) + self.async_update_listeners(new_data) + + +class PassiveBluetoothCoordinatorEntity( + Entity, Generic[_PassiveBluetoothDataUpdateCoordinatorT] +): + """A class for entities using PassiveBluetoothDataUpdateCoordinator.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + coordinator: _PassiveBluetoothDataUpdateCoordinatorT, + entity_key: PassiveBluetoothEntityKey, + description: EntityDescription, + context: Any = None, + ) -> None: + """Create the entity with a PassiveBluetoothDataUpdateCoordinator.""" + self.entity_description = description + self.entity_key = entity_key + self.coordinator = coordinator + self.coordinator_context = context + address = coordinator.address + device_id = entity_key.device_id + devices = coordinator.devices + key = entity_key.key + if device_id in devices: + base_device_info = devices[device_id] + else: + base_device_info = DeviceInfo({}) + if device_id: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}} + ) + self._attr_unique_id = f"{address}-{key}-{device_id}" + else: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, address)}} + ) + self._attr_unique_id = f"{address}-{key}" + if ATTR_NAME not in self._attr_device_info: + self._attr_device_info[ATTR_NAME] = self.coordinator.name + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.available + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.coordinator.async_add_entity_key_listener( + self._handle_coordinator_update, self.entity_key + ) + ) + + @callback + def _handle_coordinator_update( + self, new_data: PassiveBluetoothDataUpdate | None + ) -> None: + """Handle updated data from the coordinator.""" + self.async_write_ha_state() diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py new file mode 100644 index 00000000000..ca4ef6e909c --- /dev/null +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -0,0 +1,868 @@ +"""Tests for the Bluetooth integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +import time +from unittest.mock import MagicMock, patch + +from home_assistant_bluetooth import BluetoothServiceInfo + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.bluetooth.passive_update_coordinator import ( + UNAVAILABLE_SECONDS, + PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, + PassiveBluetoothEntityKey, +) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import CoreState, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.util import dt as dt_util + +from tests.common import MockEntityPlatform, async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + name="Pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_basic_usage(hass): + """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + entity_key = PassiveBluetoothEntityKey("temperature", None) + entity_key_events = [] + all_events = [] + mock_entity = MagicMock() + mock_add_entities = MagicMock() + + def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock entity key listener.""" + entity_key_events.append(data) + + cancel_async_add_entity_key_listener = coordinator.async_add_entity_key_listener( + _async_entity_key_listener, + entity_key, + ) + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + cancel_listener = coordinator.async_add_listener( + _all_listener, + ) + + cancel_async_add_entities_listener = coordinator.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 1 + assert len(all_events) == 1 + + # There should be 4 calls to create entities + assert len(mock_entity.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + + # On the second, the entities should already be created + # so the mock should not be called again + assert len(mock_entity.mock_calls) == 2 + + cancel_async_add_entity_key_listener() + cancel_listener() + cancel_async_add_entities_listener() + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should not trigger any more now + # that they were cancelled + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + assert len(mock_entity.mock_calls) == 2 + assert coordinator.available is True + + cancel_coordinator() + + +async def test_unavailable_after_no_data(hass): + """Test that the coordinator is unavailable after no data for a while.""" + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + mock_entity = MagicMock() + mock_add_entities = MagicMock() + coordinator.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + assert coordinator.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 + assert coordinator.available is True + + monotonic_now = time.monotonic() + now = dt_util.utcnow() + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", + return_value=monotonic_now + UNAVAILABLE_SECONDS, + ): + async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + await hass.async_block_till_done() + assert coordinator.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + # Now simulate the device is still present even though we got + # no data for a while + + monotonic_now = time.monotonic() + now = dt_util.utcnow() + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_address_present", + return_value=True, + ), patch( + "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", + return_value=monotonic_now + UNAVAILABLE_SECONDS, + ): + async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + await hass.async_block_till_done() + + assert coordinator.available is True + + # And finally that it can go unavailable again when its gone + monotonic_now = time.monotonic() + now = dt_util.utcnow() + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", + return_value=monotonic_now + UNAVAILABLE_SECONDS, + ): + async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + await hass.async_block_till_done() + assert coordinator.available is False + + cancel_coordinator() + + +async def test_no_updates_once_stopping(hass): + """Test updates are ignored once hass is stopping.""" + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + all_events = [] + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + coordinator.async_add_listener( + _all_listener, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + + hass.state = CoreState.stopping + + # We should stop processing events once hass is stopping + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + + cancel_coordinator() + + +async def test_exception_from_update_method(hass, caplog): + """Test we handle exceptions from the update method.""" + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + raise Exception("Test exception") + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + # We should go unavailable once we get an exception + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert "Test exception" in caplog.text + assert coordinator.available is False + + # We should go available again once we get data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + cancel_coordinator() + + +async def test_bad_data_from_update_method(hass, caplog): + """Test we handle bad data from the update method.""" + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + return "bad_data" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + # We should go unavailable once we get bad data + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert "update_method" in caplog.text + assert "bad_data" in caplog.text + assert coordinator.available is False + + # We should go available again once we get good data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + cancel_coordinator() + + +GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x04\xb5\xa2d\x00\x06L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) +GOVEE_B5178_PRIMARY_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-92, + manufacturer_data={ + 1: b"\x01\x01\x00\x03\x07Xd\x00\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) + +GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Temperature", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Humidity", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Battery", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Signal Strength", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -95, + }, +) +GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( + PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + "primary": { + "name": "B5178D6FB Primary", + "manufacturer": "Govee", + "model": "H5178", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Temperature", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Humidity", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Battery", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Signal Strength", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): SensorEntityDescription( + key="temperature_primary", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Temperature", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="primary" + ): SensorEntityDescription( + key="humidity_primary", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Humidity", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="primary" + ): SensorEntityDescription( + key="battery_primary", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Battery", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): SensorEntityDescription( + key="signal_strength_primary", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Signal Strength", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -92, + PassiveBluetoothEntityKey(key="temperature", device_id="primary"): 19.8488, + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): 48.8, + PassiveBluetoothEntityKey(key="battery", device_id="primary"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="primary"): -92, + }, + ) +) + + +async def test_integration_with_entity(hass): + """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" + + update_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal update_count + update_count += 1 + if update_count > 2: + return GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_setup() + + mock_add_entities = MagicMock() + + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Third call with primary and remote sensor entities adds the primary sensor entities + assert len(mock_add_entities.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Forth call with both primary and remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 2 + + entities = [ + *mock_add_entities.mock_calls[0][1][0], + *mock_add_entities.mock_calls[1][1][0], + ] + + entity_one: PassiveBluetoothCoordinatorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff-remote")}, + "manufacturer": "Govee", + "model": "H5178-REMOTE", + "name": "B5178D6FB Remote", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ) + + +NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={}, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + name="Pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_integration_with_entity_without_a_device(hass): + """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_setup() + + mock_add_entities = MagicMock() + + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + mock_add_entities, + ) + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + entities = mock_add_entities.mock_calls[0][1][0] + entity_one: PassiveBluetoothCoordinatorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "name": "Generic", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id=None + ) + + +async def test_passive_bluetooth_entity_with_entity_platform(hass): + """Test with a mock entity platform.""" + entity_platform = MockEntityPlatform(hass) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_setup() + + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + lambda entities: hass.async_create_task( + entity_platform.async_add_entities(entities) + ), + ) + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert hass.states.get("test_domain.temperature") is not None + assert hass.states.get("test_domain.pressure") is not None From ac858cc2b555df8e989c5ca2079adce6fec3ecb6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 21 Jul 2022 00:19:02 +0200 Subject: [PATCH 602/820] Improve singleton helper typing (#75461) * Improve singleton helper typing * Fix type errors --- .strict-typing | 1 + homeassistant/helpers/restore_state.py | 4 +--- homeassistant/helpers/singleton.py | 14 +++++++------- mypy.ini | 3 +++ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index 7616589a7b5..e64a9a04931 100644 --- a/.strict-typing +++ b/.strict-typing @@ -25,6 +25,7 @@ homeassistant.helpers.entity_values homeassistant.helpers.event homeassistant.helpers.reload homeassistant.helpers.script_variables +homeassistant.helpers.singleton homeassistant.helpers.sun homeassistant.helpers.translation homeassistant.util.async_ diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 314daecde48..73a00898d69 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -305,9 +305,7 @@ class RestoreEntity(Entity): # Return None if this entity isn't added to hass yet _LOGGER.warning("Cannot get last state. Entity not added to hass") # type: ignore[unreachable] return None - data = cast( - RestoreStateData, await RestoreStateData.async_get_instance(self.hass) - ) + data = await RestoreStateData.async_get_instance(self.hass) if self.entity_id not in data.last_states: return None return data.last_states[self.entity_id] diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index a4f6d32c303..f15806ae5ff 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -4,23 +4,23 @@ from __future__ import annotations import asyncio from collections.abc import Callable import functools -from typing import TypeVar, cast +from typing import Any, TypeVar, cast from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass _T = TypeVar("_T") -FUNC = Callable[[HomeAssistant], _T] +_FuncType = Callable[[HomeAssistant], _T] -def singleton(data_key: str) -> Callable[[FUNC], FUNC]: +def singleton(data_key: str) -> Callable[[_FuncType[_T]], _FuncType[_T]]: """Decorate a function that should be called once per instance. Result will be cached and simultaneous calls will be handled. """ - def wrapper(func: FUNC) -> FUNC: + def wrapper(func: _FuncType[_T]) -> _FuncType[_T]: """Wrap a function with caching logic.""" if not asyncio.iscoroutinefunction(func): @@ -35,10 +35,10 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: @bind_hass @functools.wraps(func) - async def async_wrapped(hass: HomeAssistant) -> _T: + async def async_wrapped(hass: HomeAssistant) -> Any: if data_key not in hass.data: evt = hass.data[data_key] = asyncio.Event() - result = await func(hass) + result = await func(hass) # type: ignore[misc] hass.data[data_key] = result evt.set() return cast(_T, result) @@ -51,6 +51,6 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: return cast(_T, obj_or_evt) - return async_wrapped + return async_wrapped # type: ignore[return-value] return wrapper diff --git a/mypy.ini b/mypy.ini index 84bc6ae00b0..ee953f13d74 100644 --- a/mypy.ini +++ b/mypy.ini @@ -87,6 +87,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.script_variables] disallow_any_generics = true +[mypy-homeassistant.helpers.singleton] +disallow_any_generics = true + [mypy-homeassistant.helpers.sun] disallow_any_generics = true From ca1f0909fbe26f9e748a0afff37d61f88e8b156d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 21 Jul 2022 02:13:10 +0200 Subject: [PATCH 603/820] Fix spelling in recorder integration (#75539) --- homeassistant/components/recorder/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 9585804690a..49e870b658f 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -295,9 +295,9 @@ class Recorder(threading.Thread): @callback def _async_check_queue(self, *_: Any) -> None: - """Periodic check of the queue size to ensure we do not exaust memory. + """Periodic check of the queue size to ensure we do not exhaust memory. - The queue grows during migraton or if something really goes wrong. + The queue grows during migration or if something really goes wrong. """ size = self.backlog _LOGGER.debug("Recorder queue size is: %s", size) From 87797c8b66787f4ede69a440f13047bd007876c3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 21 Jul 2022 00:26:18 +0000 Subject: [PATCH 604/820] [ci skip] Translation update --- .../ambiclimate/translations/hu.json | 2 +- .../components/apple_tv/translations/hu.json | 14 ++-- .../components/arcam_fmj/translations/hu.json | 2 +- .../components/asuswrt/translations/hu.json | 2 +- .../components/august/translations/hu.json | 2 +- .../azure_devops/translations/hu.json | 2 +- .../azure_event_hub/translations/hu.json | 8 +- .../components/bond/translations/hu.json | 2 +- .../components/bosch_shc/translations/hu.json | 4 +- .../components/coinbase/translations/hu.json | 2 +- .../components/control4/translations/hu.json | 2 +- .../crownstone/translations/hu.json | 2 +- .../components/demo/translations/ca.json | 21 ++++++ .../components/demo/translations/en.json | 3 - .../components/demo/translations/fr.json | 21 ++++++ .../components/demo/translations/pt-BR.json | 21 ++++++ .../components/demo/translations/zh-Hant.json | 21 ++++++ .../components/denonavr/translations/hu.json | 4 +- .../devolo_home_control/translations/hu.json | 2 +- .../components/ecobee/translations/hu.json | 2 +- .../eight_sleep/translations/ru.json | 19 +++++ .../components/esphome/translations/hu.json | 6 +- .../components/ezviz/translations/hu.json | 2 +- .../components/fivem/translations/hu.json | 2 +- .../forecast_solar/translations/hu.json | 4 +- .../forked_daapd/translations/hu.json | 4 +- .../freedompro/translations/hu.json | 2 +- .../components/generic/translations/ru.json | 2 + .../components/goalzero/translations/hu.json | 2 +- .../components/google/translations/hu.json | 5 +- .../components/google/translations/ru.json | 3 + .../google/translations/zh-Hant.json | 3 +- .../components/hassio/translations/ru.json | 1 + .../components/heos/translations/hu.json | 2 +- .../here_travel_time/translations/it.json | 3 + .../homekit_controller/translations/hu.json | 2 +- .../homekit_controller/translations/it.json | 2 +- .../components/honeywell/translations/hu.json | 2 +- .../huawei_lte/translations/hu.json | 2 +- .../components/hyperion/translations/hu.json | 2 +- .../components/iaqualink/translations/hu.json | 2 +- .../components/icloud/translations/hu.json | 2 +- .../intellifire/translations/hu.json | 2 +- .../components/iotawatt/translations/hu.json | 2 +- .../components/ipp/translations/hu.json | 2 +- .../components/knx/translations/hu.json | 4 +- .../components/konnected/translations/hu.json | 4 +- .../components/laundrify/translations/hu.json | 4 +- .../components/lifx/translations/hu.json | 20 +++++ .../components/lifx/translations/it.json | 12 +++ .../components/lifx/translations/zh-Hant.json | 20 +++++ .../logi_circle/translations/hu.json | 2 +- .../components/mazda/translations/hu.json | 2 +- .../components/meater/translations/hu.json | 2 +- .../minecraft_server/translations/hu.json | 6 +- .../components/mysensors/translations/hu.json | 2 +- .../components/nest/translations/ru.json | 14 ++++ .../components/netgear/translations/hu.json | 2 +- .../nfandroidtv/translations/hu.json | 2 +- .../ovo_energy/translations/hu.json | 2 +- .../components/plaato/translations/hu.json | 2 +- .../components/plugwise/translations/ca.json | 3 +- .../components/plugwise/translations/en.json | 3 +- .../components/plugwise/translations/et.json | 3 +- .../components/plugwise/translations/fr.json | 3 +- .../components/plugwise/translations/hu.json | 2 +- .../plugwise/translations/pt-BR.json | 3 +- .../components/plugwise/translations/ru.json | 6 +- .../plugwise/translations/zh-Hant.json | 3 +- .../components/powerwall/translations/hu.json | 2 +- .../radiotherm/translations/ru.json | 31 ++++++++ .../components/renault/translations/hu.json | 2 +- .../components/rhasspy/translations/it.json | 9 +++ .../components/roomba/translations/hu.json | 2 +- .../components/roon/translations/hu.json | 2 +- .../components/samsungtv/translations/hu.json | 6 +- .../components/scrape/translations/ru.json | 73 +++++++++++++++++++ .../sensibo/translations/sensor.ru.json | 8 ++ .../components/shelly/translations/hu.json | 2 +- .../components/sia/translations/hu.json | 4 +- .../components/skybell/translations/ru.json | 21 ++++++ .../components/smappee/translations/hu.json | 2 +- .../smartthings/translations/hu.json | 8 +- .../components/soma/translations/hu.json | 4 +- .../components/spotify/translations/hu.json | 2 +- .../steam_online/translations/hu.json | 2 +- .../steam_online/translations/ru.json | 3 + .../components/subaru/translations/hu.json | 8 +- .../system_bridge/translations/hu.json | 2 +- .../tankerkoenig/translations/ru.json | 11 ++- .../tomorrowio/translations/hu.json | 2 +- .../totalconnect/translations/ru.json | 11 +++ .../components/tractive/translations/hu.json | 2 +- .../unifiprotect/translations/it.json | 2 +- .../uptimerobot/translations/hu.json | 2 +- .../components/verisure/translations/hu.json | 17 ++++- .../components/verisure/translations/it.json | 11 +++ .../vlc_telnet/translations/hu.json | 2 +- .../components/vulcan/translations/hu.json | 6 +- .../components/webostv/translations/hu.json | 2 +- .../components/wiz/translations/hu.json | 2 +- .../xiaomi_miio/translations/hu.json | 4 +- .../components/zwave_js/translations/hu.json | 2 +- 103 files changed, 494 insertions(+), 120 deletions(-) create mode 100644 homeassistant/components/eight_sleep/translations/ru.json create mode 100644 homeassistant/components/radiotherm/translations/ru.json create mode 100644 homeassistant/components/rhasspy/translations/it.json create mode 100644 homeassistant/components/scrape/translations/ru.json create mode 100644 homeassistant/components/sensibo/translations/sensor.ru.json create mode 100644 homeassistant/components/skybell/translations/ru.json diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 3de93421f42..92ea617393e 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "K\u00e9rj\u00fck, k\u00f6vesse ezt a [link]({authorization_url}}) \u00e9s **Enged\u00e9lyezze** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", + "description": "K\u00e9rem, k\u00f6vesse ezt a [link]({authorization_url}}) \u00e9s **Enged\u00e9lyezze** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", "title": "Ambiclimate hiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 9105c545758..ae1e55bf3ba 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -5,8 +5,8 @@ "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "backoff": "Az eszk\u00f6z jelenleg nem fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmeket (lehet, hogy t\u00fal sokszor adott meg \u00e9rv\u00e9nytelen PIN-k\u00f3dot), pr\u00f3b\u00e1lkozzon \u00fajra k\u00e9s\u0151bb.", "device_did_not_pair": "A p\u00e1ros\u00edt\u00e1s folyamat\u00e1t az eszk\u00f6zr\u0151l nem pr\u00f3b\u00e1lt\u00e1k befejezni.", - "device_not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a felder\u00edt\u00e9s sor\u00e1n, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", - "inconsistent_device": "Az elv\u00e1rt protokollok nem tal\u00e1lhat\u00f3k a felder\u00edt\u00e9s sor\u00e1n. Ez \u00e1ltal\u00e1ban a multicast DNS (Zeroconf) probl\u00e9m\u00e1j\u00e1t jelzi. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni az eszk\u00f6zt.", + "device_not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a felder\u00edt\u00e9s sor\u00e1n, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", + "inconsistent_device": "Az elv\u00e1rt protokollok nem tal\u00e1lhat\u00f3k a felder\u00edt\u00e9s sor\u00e1n. Ez \u00e1ltal\u00e1ban a multicast DNS (Zeroconf) probl\u00e9m\u00e1j\u00e1t jelzi. K\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni az eszk\u00f6zt.", "ipv6_not_supported": "Az IPv6 nem t\u00e1mogatott.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", @@ -22,26 +22,26 @@ "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "\u00d6n a `{name}`, `{type}` t\u00edpus\u00fa eszk\u00f6zt k\u00e9sz\u00fcl hozz\u00e1adni a Home Assistanthoz.\n\n**A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN k\u00f3dot is meg kell adnia.**\n\nK\u00e9rj\u00fck, vegye figyelembe, hogy ezzel az integr\u00e1ci\u00f3val *nem* tudja kikapcsolni az Apple TV k\u00e9sz\u00fcl\u00e9k\u00e9t. Csak a Home Assistant m\u00e9dialej\u00e1tsz\u00f3ja fog kikapcsolni!", + "description": "\u00d6n a `{name}`, `{type}` t\u00edpus\u00fa eszk\u00f6zt k\u00e9sz\u00fcl hozz\u00e1adni a Home Assistanthoz.\n\n**A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN k\u00f3dot is meg kell adnia.**\n\nK\u00e9rem, vegye figyelembe, hogy ezzel az integr\u00e1ci\u00f3val *nem* tudja kikapcsolni az Apple TV k\u00e9sz\u00fcl\u00e9k\u00e9t. Csak a Home Assistant m\u00e9dialej\u00e1tsz\u00f3ja fog kikapcsolni!", "title": "Apple TV sikeresen hozz\u00e1adva" }, "pair_no_pin": { - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rj\u00fck, \u00edrja be k\u00e9sz\u00fcl\u00e9ken a PIN k\u00f3dot: {pin}.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rem, \u00edrja be k\u00e9sz\u00fcl\u00e9ken a PIN k\u00f3dot: {pin}.", "title": "P\u00e1ros\u00edt\u00e1s" }, "pair_with_pin": { "data": { "pin": "PIN-k\u00f3d" }, - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} protokollhoz. K\u00e9rem, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", "title": "P\u00e1ros\u00edt\u00e1s" }, "password": { - "description": "`{protocol}` jelsz\u00f3t ig\u00e9nyel. Ez m\u00e9g nem t\u00e1mogatott, k\u00e9rj\u00fck, a folytat\u00e1shoz tiltsa le a jelsz\u00f3t.", + "description": "`{protocol}` jelsz\u00f3t ig\u00e9nyel. Ez m\u00e9g nem t\u00e1mogatott, k\u00e9rem, a folytat\u00e1shoz tiltsa le a jelsz\u00f3t.", "title": "Jelsz\u00f3 sz\u00fcks\u00e9ges" }, "protocol_disabled": { - "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protocol}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rj\u00fck, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", + "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protocol}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rem, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", "title": "A p\u00e1ros\u00edt\u00e1s nem lehets\u00e9ges" }, "reconfigure": { diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index 897b462eb48..0e693857363 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -19,7 +19,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t" + "description": "K\u00e9rem, adja meg az eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t" } } }, diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json index 34581ec6b7a..3627f1f22eb 100644 --- a/homeassistant/components/asuswrt/translations/hu.json +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -8,7 +8,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", "pwd_and_ssh": "Csak jelsz\u00f3 vagy SSH kulcsf\u00e1jlt adjon meg", - "pwd_or_ssh": "K\u00e9rj\u00fck, adja meg a jelsz\u00f3t vagy az SSH kulcsf\u00e1jlt", + "pwd_or_ssh": "K\u00e9rem, adja meg a jelsz\u00f3t vagy az SSH kulcsf\u00e1jlt", "ssh_not_file": "Az SSH kulcsf\u00e1jl nem tal\u00e1lhat\u00f3", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index 42f9860bdc2..68953d8b888 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -30,7 +30,7 @@ "data": { "code": "Ellen\u0151rz\u0151 k\u00f3d" }, - "description": "K\u00e9rj\u00fck, ellen\u0151rizze a {login_method} ({username}), \u00e9s \u00edrja be al\u00e1bb az ellen\u0151rz\u0151 k\u00f3dot", + "description": "K\u00e9rem, ellen\u0151rizze a {login_method} ({username}), \u00e9s \u00edrja be al\u00e1bb az ellen\u0151rz\u0151 k\u00f3dot", "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" } } diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json index 2d8879b9d68..ed0dd7f4d1e 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -15,7 +15,7 @@ "data": { "personal_access_token": "Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token (PAT)" }, - "description": "{project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", + "description": "{project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rem, adja meg az aktu\u00e1lis hiteles\u00edt\u0151 adatait.", "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/azure_event_hub/translations/hu.json b/homeassistant/components/azure_event_hub/translations/hu.json index 21b6a4ba63f..e0c50a4ae56 100644 --- a/homeassistant/components/azure_event_hub/translations/hu.json +++ b/homeassistant/components/azure_event_hub/translations/hu.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "cannot_connect": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz.", + "cannot_connect": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rem, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "unknown": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz." + "unknown": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rem, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -15,7 +15,7 @@ "data": { "event_hub_connection_string": "Event Hub csatlakoz\u00e1si karaktersor" }, - "description": "K\u00e9rj\u00fck, adja meg a csatlakoz\u00e1si karaktersort ehhez: {event_hub_instance_name}", + "description": "K\u00e9rem, adja meg a csatlakoz\u00e1si karaktersort ehhez: {event_hub_instance_name}", "title": "Csatlakoz\u00e1si karaktersor t\u00edpus" }, "sas": { @@ -24,7 +24,7 @@ "event_hub_sas_key": "Event Hub SAS kulcs", "event_hub_sas_policy": "Event Hub SAS h\u00e1zirend" }, - "description": "K\u00e9rj\u00fck, adja meg a SAS hiteles\u00edt\u0151 adatait ehhez: {event_hub_instance_name}", + "description": "K\u00e9rem, adja meg a SAS hiteles\u00edt\u0151 adatait ehhez: {event_hub_instance_name}", "title": "SAS hiteles\u00edt\u00e9s t\u00edpus" }, "user": { diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json index 179ec599d9f..801a3bf8a44 100644 --- a/homeassistant/components/bond/translations/hu.json +++ b/homeassistant/components/bond/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtse, miel\u0151tt folytatn\u00e1", + "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rem friss\u00edtse, miel\u0151tt folytatn\u00e1", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json index df5a484cabe..2008965d95d 100644 --- a/homeassistant/components/bosch_shc/translations/hu.json +++ b/homeassistant/components/bosch_shc/translations/hu.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "pairing_failed": "A p\u00e1ros\u00edt\u00e1s nem siker\u00fclt; K\u00e9rj\u00fck, ellen\u0151rizze, hogy a Bosch Smart Home Controller p\u00e1ros\u00edt\u00e1si m\u00f3dban van-e (villog a LED), \u00e9s hogy a jelszava helyes-e.", + "pairing_failed": "A p\u00e1ros\u00edt\u00e1s nem siker\u00fclt; K\u00e9rem, ellen\u0151rizze, hogy a Bosch Smart Home Controller p\u00e1ros\u00edt\u00e1si m\u00f3dban van-e (villog a LED), \u00e9s hogy a jelszava helyes-e.", "session_error": "Munkamenet hiba: Az API nem OK eredm\u00e9nyt ad vissza.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "K\u00e9rj\u00fck, addig nyomja a Bosch Smart Home Controller el\u00fcls\u0151 gombj\u00e1t, am\u00edg a LED villogni nem kezd.\nK\u00e9szen \u00e1ll {model} @ {host} be\u00e1ll\u00edt\u00e1s\u00e1nak folytat\u00e1s\u00e1ra Home Assistant seg\u00edts\u00e9g\u00e9vel?" + "description": "K\u00e9rem, addig nyomja a Bosch Smart Home Controller el\u00fcls\u0151 gombj\u00e1t, am\u00edg a LED villogni nem kezd.\nK\u00e9szen \u00e1ll {model} @ {host} be\u00e1ll\u00edt\u00e1s\u00e1nak folytat\u00e1s\u00e1ra Home Assistant seg\u00edts\u00e9g\u00e9vel?" }, "credentials": { "data": { diff --git a/homeassistant/components/coinbase/translations/hu.json b/homeassistant/components/coinbase/translations/hu.json index bcf409d2dda..54122d29966 100644 --- a/homeassistant/components/coinbase/translations/hu.json +++ b/homeassistant/components/coinbase/translations/hu.json @@ -16,7 +16,7 @@ "api_key": "API kulcs", "api_token": "API jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg API kulcs\u00e1nak adatait a Coinbase \u00e1ltal megadott m\u00f3don.", + "description": "K\u00e9rem, adja meg API kulcs\u00e1nak adatait a Coinbase \u00e1ltal megadott m\u00f3don.", "title": "Coinbase API kulcs r\u00e9szletei" } } diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json index 5d41eb09a84..033555af95e 100644 --- a/homeassistant/components/control4/translations/hu.json +++ b/homeassistant/components/control4/translations/hu.json @@ -15,7 +15,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg Control4-fi\u00f3kj\u00e1nak adatait \u00e9s a helyi vez\u00e9rl\u0151 IP-c\u00edm\u00e9t." + "description": "K\u00e9rem, adja meg Control4-fi\u00f3kj\u00e1nak adatait \u00e9s a helyi vez\u00e9rl\u0151 IP-c\u00edm\u00e9t." } } }, diff --git a/homeassistant/components/crownstone/translations/hu.json b/homeassistant/components/crownstone/translations/hu.json index f2ffe6f2b07..1d7c6efe2ea 100644 --- a/homeassistant/components/crownstone/translations/hu.json +++ b/homeassistant/components/crownstone/translations/hu.json @@ -6,7 +6,7 @@ "usb_setup_unsuccessful": "A Crownstone USB be\u00e1ll\u00edt\u00e1sa sikertelen volt." }, "error": { - "account_not_verified": "Nem ellen\u0151rz\u00f6tt fi\u00f3k. K\u00e9rj\u00fck, aktiv\u00e1lja fi\u00f3kj\u00e1t a Crownstone-t\u00f3l kapott aktiv\u00e1l\u00f3 e-mailben.", + "account_not_verified": "Nem ellen\u0151rz\u00f6tt fi\u00f3k. K\u00e9rem, aktiv\u00e1lja fi\u00f3kj\u00e1t a Crownstone-t\u00f3l kapott aktiv\u00e1l\u00f3 e-mailben.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index dbccaaf24a2..4ffda91d97c 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Prem D'acord quan s'hagi omplert el l\u00edquid d'intermitents", + "title": "Cal omplir el l\u00edquid d'intermitents" + } + } + }, + "title": "El l\u00edquid d'intermitents est\u00e0 buit i s'ha d'omplir" + }, + "transmogrifier_deprecated": { + "description": "El component 'transmogrifier' est\u00e0 obsolet, ja que el control local ja no est\u00e0 disponible a la nova API", + "title": "El component 'transmogrifier' est\u00e0 obsolet" + }, + "unfixable_problem": { + "description": "Aquest problema no es rendir\u00e0 mai.", + "title": "No \u00e9s un problema solucionable" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/en.json b/homeassistant/components/demo/translations/en.json index 326e741f764..11378fb94d4 100644 --- a/homeassistant/components/demo/translations/en.json +++ b/homeassistant/components/demo/translations/en.json @@ -22,9 +22,6 @@ }, "options": { "step": { - "init": { - "data": {} - }, "options_1": { "data": { "bool": "Optional boolean", diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index 2f979d80a32..c5a150d8cb6 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Appuyez sur OK une fois le liquide de clignotant rempli", + "title": "Le liquide de clignotant doit \u00eatre rempli" + } + } + }, + "title": "Le r\u00e9servoir de liquide de clignotant est vide et doit \u00eatre rempli" + }, + "transmogrifier_deprecated": { + "description": "Le composant de transmogrification est d\u00e9sormais obsol\u00e8te en raison de l'absence de contr\u00f4le local dans la nouvelle API", + "title": "Le composant de transmogrification est obsol\u00e8te" + }, + "unfixable_problem": { + "description": "Ce probl\u00e8me ne va jamais s'arr\u00eater.", + "title": "Ce probl\u00e8me ne peut \u00eatre corrig\u00e9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/pt-BR.json b/homeassistant/components/demo/translations/pt-BR.json index 49290be4ceb..26e903f345d 100644 --- a/homeassistant/components/demo/translations/pt-BR.json +++ b/homeassistant/components/demo/translations/pt-BR.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pressione OK quando o fluido do pisca-pisca for reabastecido", + "title": "O fluido do pisca-pisca precisa ser reabastecido" + } + } + }, + "title": "O fluido do pisca-pisca est\u00e1 vazio e precisa ser reabastecido" + }, + "transmogrifier_deprecated": { + "description": "O componente transmogriifier agora est\u00e1 obsoleto devido \u00e0 falta de controle local dispon\u00edvel na nova API", + "title": "O componente transmogriificador est\u00e1 obsoleto" + }, + "unfixable_problem": { + "description": "Esta quest\u00e3o nunca vai desistir.", + "title": "Este n\u00e3o \u00e9 um problema corrig\u00edvel" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/zh-Hant.json b/homeassistant/components/demo/translations/zh-Hant.json index f9f798134ba..a984c9c62ca 100644 --- a/homeassistant/components/demo/translations/zh-Hant.json +++ b/homeassistant/components/demo/translations/zh-Hant.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u65bc\u8a0a\u865f\u5291\u88dc\u5145\u5f8c\u6309\u4e0b OK", + "title": "\u8a0a\u865f\u5291\u9700\u8981\u88dc\u5145" + } + } + }, + "title": "\u8a0a\u865f\u5291\u5df2\u7528\u5b8c\u3001\u9700\u8981\u91cd\u65b0\u88dc\u5145" + }, + "transmogrifier_deprecated": { + "description": "\u7531\u65bc\u65b0 API \u7f3a\u4e4f\u672c\u5730\u7aef\u63a7\u5236\u652f\u63f4\u3001Transmogrifier \u5143\u4ef6\u5df2\u7d93\u4e0d\u63a8\u85a6\u4f7f\u7528", + "title": "Transmogrifier \u5143\u4ef6\u5df2\u7d93\u4e0d\u63a8\u85a6\u4f7f\u7528" + }, + "unfixable_problem": { + "description": "\u9019\u554f\u984c\u7e3d\u662f\u4e0d\u6b7b\u5fc3\u7684\u51fa\u73fe\u3002", + "title": "\u9019\u4e0d\u662f\u4e00\u500b\u53ef\u4fee\u7684\u554f\u984c" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index 8c51c7e990f..302a485395f 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", - "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet", + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rem, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet", "not_denonavr_manufacturer": "Nem egy Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedezett gy\u00e1rt\u00f3n\u00e9v nem megfelel\u0151", "not_denonavr_missing": "Nem Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedez\u00e9si inform\u00e1ci\u00f3k nem teljesek" }, @@ -13,7 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "K\u00e9rj\u00fck, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t" + "description": "K\u00e9rem, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t" }, "select": { "data": { diff --git a/homeassistant/components/devolo_home_control/translations/hu.json b/homeassistant/components/devolo_home_control/translations/hu.json index 391eeb60727..b7fabbbef2f 100644 --- a/homeassistant/components/devolo_home_control/translations/hu.json +++ b/homeassistant/components/devolo_home_control/translations/hu.json @@ -6,7 +6,7 @@ }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "reauth_failed": "K\u00e9rj\u00fck, ugyanazt a mydevolo felhaszn\u00e1l\u00f3t haszn\u00e1lja, mint kor\u00e1bban." + "reauth_failed": "K\u00e9rem, ugyanazt a mydevolo felhaszn\u00e1l\u00f3t haszn\u00e1lja, mint kor\u00e1bban." }, "step": { "user": { diff --git a/homeassistant/components/ecobee/translations/hu.json b/homeassistant/components/ecobee/translations/hu.json index a2fdd5553a4..590cc066de9 100644 --- a/homeassistant/components/ecobee/translations/hu.json +++ b/homeassistant/components/ecobee/translations/hu.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "K\u00e9rj\u00fck, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n {pin} \n \nEzut\u00e1n nyomja meg a Mehet gombot.", + "description": "K\u00e9rem, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n{pin} \n \nEzut\u00e1n nyomja meg a Mehet gombot.", "title": "Alkalmaz\u00e1s enged\u00e9lyez\u00e9se ecobee.com-on" }, "user": { diff --git a/homeassistant/components/eight_sleep/translations/ru.json b/homeassistant/components/eight_sleep/translations/ru.json new file mode 100644 index 00000000000..0370f202938 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043e\u0431\u043b\u0430\u043a\u0443 Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043e\u0431\u043b\u0430\u043a\u0443 Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 3eaf276b309..1f98953678c 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -8,7 +8,7 @@ "error": { "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML konfigur\u00e1ci\u00f3 tartalmaz egy \"api:\" sort.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "invalid_psk": "Az adat\u00e1tviteli titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen. K\u00e9rj\u00fck, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy megegyezik a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151vel.", + "invalid_psk": "Az adat\u00e1tviteli titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy megegyezik a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151vel.", "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rem, \u00e1ll\u00edtson be egy statikus IP-c\u00edmet." }, "flow_title": "{name}", @@ -27,13 +27,13 @@ "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151, {name} titkos\u00edt\u00e1si kulcs\u00e1t." + "description": "K\u00e9rem, adja meg a bekonfigur\u00e1lt titkos\u00edt\u00e1si kulcsot: {name}" }, "reauth_confirm": { "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "{name} ESPHome v\u00e9gpont aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rj\u00fck, adja meg az aktu\u00e1lis kulcsot." + "description": "{name} ESPHome v\u00e9gpont aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rem, adja meg az aktu\u00e1lis titkos\u00edt\u00e1si kulcsot." }, "user": { "data": { diff --git a/homeassistant/components/ezviz/translations/hu.json b/homeassistant/components/ezviz/translations/hu.json index 5907f66ceb4..49cd4b43d08 100644 --- a/homeassistant/components/ezviz/translations/hu.json +++ b/homeassistant/components/ezviz/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "ezviz_cloud_account_missing": "Ezviz cloud fi\u00f3k hi\u00e1nyzik. K\u00e9rj\u00fck, konfigur\u00e1lja \u00fajra az Ezviz cloud fi\u00f3kot.", + "ezviz_cloud_account_missing": "Ezviz cloud fi\u00f3k hi\u00e1nyzik. K\u00e9rem, konfigur\u00e1lja \u00fajra az Ezviz cloud fi\u00f3kot.", "unknown": "V\u00e1ratlan hiba" }, "error": { diff --git a/homeassistant/components/fivem/translations/hu.json b/homeassistant/components/fivem/translations/hu.json index d29d0c9a802..28068946c16 100644 --- a/homeassistant/components/fivem/translations/hu.json +++ b/homeassistant/components/fivem/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l is, hogy a leg\u00fajabb FiveM szervert futtatja.", + "cannot_connect": "Nem siker\u00fclt csatlakozni. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l is, hogy a leg\u00fajabb FiveM szervert futtatja.", "invalid_game_name": "A j\u00e1t\u00e9k API-ja, amelyhez csatlakozni pr\u00f3b\u00e1l, nem FiveM.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json index 33a69ad2fd7..3aac09afe1b 100644 --- a/homeassistant/components/forecast_solar/translations/hu.json +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -10,7 +10,7 @@ "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)", "name": "Elnevez\u00e9s" }, - "description": "T\u00f6ltse ki a napelemek adatait. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." + "description": "T\u00f6ltse ki a napelemek adatait. K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." } } }, @@ -25,7 +25,7 @@ "inverter_size": "Inverter m\u00e9rete (Watt)", "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)" }, - "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." + "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 kit\u00f6lt\u00e9se nem egy\u00e9rtelm\u0171." } } } diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index 2058bbd1cbe..51285d2f7d4 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -5,10 +5,10 @@ "not_forked_daapd": "Az eszk\u00f6z nem forked-daapd kiszolg\u00e1l\u00f3." }, "error": { - "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", + "forbidden": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "websocket_not_enabled": "forked-daapd szerver websocket nincs enged\u00e9lyezve.", - "wrong_host_or_port": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot.", + "wrong_host_or_port": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot.", "wrong_password": "Helytelen jelsz\u00f3.", "wrong_server_type": "A forked-daapd integr\u00e1ci\u00f3hoz forked-daapd szerver sz\u00fcks\u00e9ges, amelynek verzi\u00f3ja legal\u00e1bb 27.0." }, diff --git a/homeassistant/components/freedompro/translations/hu.json b/homeassistant/components/freedompro/translations/hu.json index e56cc7a4a41..dd916b5b11a 100644 --- a/homeassistant/components/freedompro/translations/hu.json +++ b/homeassistant/components/freedompro/translations/hu.json @@ -12,7 +12,7 @@ "data": { "api_key": "API kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg a https://home.freedompro.eu webhelyr\u0151l kapott API-kulcsot", + "description": "K\u00e9rem, adja meg a https://home.freedompro.eu webhelyr\u0151l kapott API-kulcsot", "title": "Freedompro API kulcs" } } diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json index 7314a172867..022af07b58b 100644 --- a/homeassistant/components/generic/translations/ru.json +++ b/homeassistant/components/generic/translations/ru.json @@ -17,6 +17,7 @@ "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "template_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0448\u0430\u0431\u043b\u043e\u043d\u0430. \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." @@ -61,6 +62,7 @@ "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "template_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0448\u0430\u0431\u043b\u043e\u043d\u0430. \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json index c0376fc29b9..607cc4e5b63 100644 --- a/homeassistant/components/goalzero/translations/hu.json +++ b/homeassistant/components/goalzero/translations/hu.json @@ -19,7 +19,7 @@ "host": "C\u00edm", "name": "Elnevez\u00e9s" }, - "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." + "description": "K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." } } } diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json index 0ff516dcfed..b27e06b15c7 100644 --- a/homeassistant/components/google/translations/hu.json +++ b/homeassistant/components/google/translations/hu.json @@ -7,11 +7,12 @@ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "code_expired": "A hiteles\u00edt\u00e9si k\u00f3d lej\u00e1rt vagy a hiteles\u00edt\u0151 adatok be\u00e1ll\u00edt\u00e1sa \u00e9rv\u00e9nytelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra.", + "code_expired": "A hiteles\u00edt\u00e9si k\u00f3d lej\u00e1rt vagy a hiteles\u00edt\u0151 adatok be\u00e1ll\u00edt\u00e1sa \u00e9rv\u00e9nytelen, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra.", "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "oauth_error": "\u00c9rv\u00e9nytelen token adatok \u00e9rkeztek.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n" }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 5d9f51fe14e..5fc2cb03feb 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044e Google. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0432\u0430\u0448\u0438\u043c \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u0435\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n3. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **TV and Limited Input devices** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0422\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + }, "config": { "abort": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index bee271ea8e7..45dc83c1b3c 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -11,7 +11,8 @@ "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index f9572edcd6f..5e1caa41ebf 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u0430\u0433\u0435\u043d\u0442\u0430", "board": "\u041f\u043b\u0430\u0442\u0430", "disk_total": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", "disk_used": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u043e", diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json index 8996c2a4530..49bee7bac8f 100644 --- a/homeassistant/components/heos/translations/hu.json +++ b/homeassistant/components/heos/translations/hu.json @@ -11,7 +11,7 @@ "data": { "host": "C\u00edm" }, - "description": "K\u00e9rj\u00fck, adja meg egy Heos-eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t (lehet\u0151leg egy vezet\u00e9kkel a h\u00e1l\u00f3zathoz csatlakoztatott eszk\u00f6zt).", + "description": "K\u00e9rem, adja meg egy Heos-eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t (lehet\u0151leg egy vezet\u00e9kes h\u00e1l\u00f3zathoz csatlakoztatott eszk\u00f6zt).", "title": "Csatlakoz\u00e1s a Heos-hoz" } } diff --git a/homeassistant/components/here_travel_time/translations/it.json b/homeassistant/components/here_travel_time/translations/it.json index e9716318adb..7c87ef36d0e 100644 --- a/homeassistant/components/here_travel_time/translations/it.json +++ b/homeassistant/components/here_travel_time/translations/it.json @@ -39,6 +39,9 @@ }, "title": "Scegli la partenza" }, + "origin_menu": { + "title": "Scegli Origine" + }, "user": { "data": { "api_key": "Chiave API", diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index 607c46ede6d..c8f86e46f38 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -11,7 +11,7 @@ "no_devices": "Nem tal\u00e1lhat\u00f3 nem p\u00e1ros\u00edtott eszk\u00f6z" }, "error": { - "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rj\u00fck, ellen\u0151rizze, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rem, ellen\u0151rizze, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", "insecure_setup_code": "A k\u00e9rt telep\u00edt\u00e9si k\u00f3d trivi\u00e1lis jellege miatt nem biztons\u00e1gos. Ez a tartoz\u00e9k nem felel meg az alapvet\u0151 biztons\u00e1gi k\u00f6vetelm\u00e9nyeknek.", "max_peers_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel nincs szabad p\u00e1ros\u00edt\u00e1si t\u00e1rhelye.", "pairing_failed": "Nem kezelt hiba t\u00f6rt\u00e9nt az eszk\u00f6zzel val\u00f3 p\u00e1ros\u00edt\u00e1s sor\u00e1n. Lehet, hogy ez \u00e1tmeneti hiba, vagy az eszk\u00f6z jelenleg m\u00e9g nem t\u00e1mogatott.", diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index d95eff05cea..947e03374b4 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -18,7 +18,7 @@ "unable_to_pair": "Impossibile abbinare, riprova.", "unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Interrompi l'associazione su tutti i controller o provare a riavviare il dispositivo, quindi continua a riprendere l'associazione.", diff --git a/homeassistant/components/honeywell/translations/hu.json b/homeassistant/components/honeywell/translations/hu.json index 14ba9167ea5..b0552b23fe1 100644 --- a/homeassistant/components/honeywell/translations/hu.json +++ b/homeassistant/components/honeywell/translations/hu.json @@ -9,7 +9,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat." + "description": "K\u00e9rem, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat." } } }, diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index c01a5cb4bb8..e1a26405396 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -9,7 +9,7 @@ "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_url": "\u00c9rv\u00e9nytelen URL", - "login_attempts_exceeded": "T\u00fall\u00e9pte a maxim\u00e1lis bejelentkez\u00e9si k\u00eds\u00e9rleteket. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb", + "login_attempts_exceeded": "T\u00fall\u00e9pte a maxim\u00e1lis bejelentkez\u00e9si k\u00eds\u00e9rleteket. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb.", "response_error": "Ismeretlen hiba az eszk\u00f6zr\u0151l", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index aaffa705279..abb02b1f6e7 100644 --- a/homeassistant/components/hyperion/translations/hu.json +++ b/homeassistant/components/hyperion/translations/hu.json @@ -27,7 +27,7 @@ "title": "Er\u0151s\u00edtse meg a Hyperion Ambilight szolg\u00e1ltat\u00e1s hozz\u00e1ad\u00e1s\u00e1t" }, "create_token": { - "description": "Az al\u00e1bbiakban v\u00e1lassza a **Mehet** lehet\u0151s\u00e9get \u00faj hiteles\u00edt\u00e9si token k\u00e9r\u00e9s\u00e9hez. A k\u00e9relem j\u00f3v\u00e1hagy\u00e1s\u00e1hoz \u00e1tir\u00e1ny\u00edtunk a Hyperion felhaszn\u00e1l\u00f3i fel\u00fcletre. K\u00e9rj\u00fck, ellen\u0151rizze, hogy a megjelen\u00edtett azonos\u00edt\u00f3 \"{auth_id}\"", + "description": "Az al\u00e1bbiakban v\u00e1lassza a **Mehet** lehet\u0151s\u00e9get \u00faj hiteles\u00edt\u00e9si token k\u00e9r\u00e9s\u00e9hez. A k\u00e9relem j\u00f3v\u00e1hagy\u00e1s\u00e1hoz \u00e1tir\u00e1ny\u00edtunk a Hyperion felhaszn\u00e1l\u00f3i fel\u00fcletre. K\u00e9rem, ellen\u0151rizze, hogy a megjelen\u00edtett azonos\u00edt\u00f3 \"{auth_id}\"", "title": "\u00daj hiteles\u00edt\u00e9si token automatikus l\u00e9trehoz\u00e1sa" }, "create_token_external": { diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json index 4ee5ba39b8b..7bbac17747a 100644 --- a/homeassistant/components/iaqualink/translations/hu.json +++ b/homeassistant/components/iaqualink/translations/hu.json @@ -13,7 +13,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg iAqualink-fi\u00f3kj\u00e1nak felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", + "description": "K\u00e9rem, adja meg iAqualink-fi\u00f3kj\u00e1nak felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", "title": "Csatlakoz\u00e1s az iAqualinkhez" } } diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index cff637ae03f..1cbbbfb6974 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -38,7 +38,7 @@ "data": { "verification_code": "Ellen\u0151rz\u0151 k\u00f3d" }, - "description": "K\u00e9rj\u00fck, \u00edrja be az iCloud-t\u00f3l \u00e9ppen kapott ellen\u0151rz\u0151 k\u00f3dot", + "description": "K\u00e9rem, \u00edrja be az iCloud-t\u00f3l \u00e9ppen kapott ellen\u0151rz\u0151 k\u00f3dot", "title": "iCloud ellen\u0151rz\u0151 k\u00f3d" } } diff --git a/homeassistant/components/intellifire/translations/hu.json b/homeassistant/components/intellifire/translations/hu.json index 2f680f2e776..afd5b99e8d7 100644 --- a/homeassistant/components/intellifire/translations/hu.json +++ b/homeassistant/components/intellifire/translations/hu.json @@ -31,7 +31,7 @@ "data": { "host": "C\u00edm" }, - "description": "A k\u00f6vetkez\u0151 IntelliFire eszk\u00f6z\u00f6k \u00e9szlelve. K\u00e9rj\u00fck, v\u00e1lassza ki, melyiket szeretn\u00e9 konfigur\u00e1lni.", + "description": "A k\u00f6vetkez\u0151 IntelliFire eszk\u00f6z\u00f6k \u00e9szlelve. K\u00e9rem, v\u00e1lassza ki, melyiket szeretn\u00e9 konfigur\u00e1lni.", "title": "Eszk\u00f6z v\u00e1laszt\u00e1sa" } } diff --git a/homeassistant/components/iotawatt/translations/hu.json b/homeassistant/components/iotawatt/translations/hu.json index 5f5d30136da..0c048e0102c 100644 --- a/homeassistant/components/iotawatt/translations/hu.json +++ b/homeassistant/components/iotawatt/translations/hu.json @@ -11,7 +11,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Az IoTawatt eszk\u00f6z hiteles\u00edt\u00e9st ig\u00e9nyel. K\u00e9rj\u00fck, adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t, majd folytassa." + "description": "Az IoTawatt eszk\u00f6z hiteles\u00edt\u00e9st ig\u00e9nyel. K\u00e9rem, adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t, majd folytassa." }, "user": { "data": { diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index 18381fde2cf..471c73c8abc 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -11,7 +11,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra az SSL/TLS opci\u00f3 bejel\u00f6l\u00e9s\u00e9vel." + "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz. K\u00e9rem, esetleg pr\u00f3b\u00e1lja meg \u00fajra az SSL/TLS opci\u00f3 bejel\u00f6l\u00e9s\u00e9vel." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 28cab2cea5f..92411b58312 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -48,7 +48,7 @@ "knxkeys_filename": "A f\u00e1jl a `.storage/knx/` konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r\u00e1ban helyezend\u0151.\nHome Assistant oper\u00e1ci\u00f3s rendszer eset\u00e9n ez a k\u00f6vetkez\u0151 lenne: `/config/.storage/knx/`\nP\u00e9lda: \"my_project.knxkeys\".", "knxkeys_password": "Ez a be\u00e1ll\u00edt\u00e1s a f\u00e1jl ETS-b\u0151l t\u00f6rt\u00e9n\u0151 export\u00e1l\u00e1sakor t\u00f6rt\u00e9nt." }, - "description": "K\u00e9rj\u00fck, adja meg a '.knxkeys' f\u00e1jl adatait." + "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." }, "secure_manual": { "data": { @@ -61,7 +61,7 @@ "user_id": "Ez gyakran a tunnel sz\u00e1ma +1. Teh\u00e1t a \"Tunnel 2\" felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja \"3\".", "user_password": "Jelsz\u00f3 az adott tunnelhez, amely a tunnel \u201eProperties\u201d panelj\u00e9n van be\u00e1ll\u00edtva az ETS-ben." }, - "description": "K\u00e9rj\u00fck, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat." }, "secure_tunneling": { "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index 57cbf0d9bb9..914546e4481 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -24,7 +24,7 @@ "host": "IP c\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg a Konnected Panel csatlakoz\u00e1si adatait." + "description": "K\u00e9rem, adja meg a Konnected Panel csatlakoz\u00e1si adatait." } } }, @@ -91,7 +91,7 @@ "discovery": "V\u00e1laszoljon a h\u00e1l\u00f3zaton \u00e9rkez\u0151 felder\u00edt\u00e9si k\u00e9r\u00e9sekre", "override_api_host": "Az alap\u00e9rtelmezett Home Assistant API host-URL fel\u00fcl\u00edr\u00e1sa" }, - "description": "K\u00e9rj\u00fck, v\u00e1lassza ki a k\u00edv\u00e1nt viselked\u00e9st a panelhez", + "description": "K\u00e9rem, v\u00e1lassza ki panel k\u00edv\u00e1nt m\u0171k\u00f6d\u00e9s\u00e9t", "title": "Egy\u00e9b be\u00e1ll\u00edt\u00e1sa" }, "options_switch": { diff --git a/homeassistant/components/laundrify/translations/hu.json b/homeassistant/components/laundrify/translations/hu.json index aa22ddc3da3..604bffbec52 100644 --- a/homeassistant/components/laundrify/translations/hu.json +++ b/homeassistant/components/laundrify/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "invalid_format": "\u00c9rv\u00e9nytelen form\u00e1tum. K\u00e9rj\u00fck, adja meg \u00edgy: xxx-xxx.", + "invalid_format": "\u00c9rv\u00e9nytelen form\u00e1tum. K\u00e9rem, \u00edgy adja meg: xxx-xxx.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -14,7 +14,7 @@ "data": { "code": "Hiteles\u00edt\u00e9si k\u00f3d (xxx-xxx)" }, - "description": "K\u00e9rj\u00fck, adja meg a laundrify-alkalmaz\u00e1sban megjelen\u0151 szem\u00e9lyes enged\u00e9lyez\u00e9si k\u00f3dj\u00e1t." + "description": "K\u00e9rem, adja meg a laundrify-alkalmaz\u00e1sban megjelen\u0151 szem\u00e9lyes enged\u00e9lyez\u00e9si k\u00f3dj\u00e1t." }, "reauth_confirm": { "description": "A laundrify integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie.", diff --git a/homeassistant/components/lifx/translations/hu.json b/homeassistant/components/lifx/translations/hu.json index 3d728f21d07..588d5932e10 100644 --- a/homeassistant/components/lifx/translations/hu.json +++ b/homeassistant/components/lifx/translations/hu.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: LIFX?" + }, + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Eszk\u00f6z" + } + }, + "user": { + "data": { + "host": "C\u00edm" + }, + "description": "Ha \u00fcresen hagyja a c\u00edm mez\u0151t, a rendszer megpr\u00f3b\u00e1lja az eszk\u00f6z\u00f6ket megkeresni." } } } diff --git a/homeassistant/components/lifx/translations/it.json b/homeassistant/components/lifx/translations/it.json index be167ec9994..d9c25e63590 100644 --- a/homeassistant/components/lifx/translations/it.json +++ b/homeassistant/components/lifx/translations/it.json @@ -4,9 +4,21 @@ "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Vuoi configurare LIFX?" + }, + "discovery_confirm": { + "description": "Vuoi configurare {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "description": "Se lasci l'host vuoto, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi." } } } diff --git a/homeassistant/components/lifx/translations/zh-Hant.json b/homeassistant/components/lifx/translations/zh-Hant.json index 911eaa570d1..e8ff08be901 100644 --- a/homeassistant/components/lifx/translations/zh-Hant.json +++ b/homeassistant/components/lifx/translations/zh-Hant.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a LIFX\uff1f" + }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {label} ({host}) {serial}\uff1f" + }, + "pick_device": { + "data": { + "device": "\u88dd\u7f6e" + } + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/logi_circle/translations/hu.json b/homeassistant/components/logi_circle/translations/hu.json index 947f11e4907..1d24638705e 100644 --- a/homeassistant/components/logi_circle/translations/hu.json +++ b/homeassistant/components/logi_circle/translations/hu.json @@ -13,7 +13,7 @@ }, "step": { "auth": { - "description": "K\u00e9rj\u00fck, k\u00f6vesse az al\u00e1bbi linket, \u00e9s ** Fogadja el ** a LogiCircle -fi\u00f3kj\u00e1hoz val\u00f3 hozz\u00e1f\u00e9r\u00e9st, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot. \n\n [Link]({authorization_url})", + "description": "K\u00e9rem, k\u00f6vesse az al\u00e1bbi linket, \u00e9s **Fogadja el** a LogiCircle -fi\u00f3kj\u00e1hoz val\u00f3 hozz\u00e1f\u00e9r\u00e9st, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot. \n\n[Link]({authorization_url})", "title": "Hiteles\u00edt\u00e9s a LogiCircle seg\u00edts\u00e9g\u00e9vel" }, "user": { diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json index 42afc763687..baa251ac301 100644 --- a/homeassistant/components/mazda/translations/hu.json +++ b/homeassistant/components/mazda/translations/hu.json @@ -17,7 +17,7 @@ "password": "Jelsz\u00f3", "region": "R\u00e9gi\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg azt az e-mail c\u00edmet \u00e9s jelsz\u00f3t, amelyet a MyMazda mobilalkalmaz\u00e1sba val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt." + "description": "K\u00e9rem, adja meg azt az e-mail c\u00edmet \u00e9s jelsz\u00f3t, amelyet a MyMazda mobilalkalmaz\u00e1sba val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt." } } } diff --git a/homeassistant/components/meater/translations/hu.json b/homeassistant/components/meater/translations/hu.json index 964c240b4e3..98f54598ee4 100644 --- a/homeassistant/components/meater/translations/hu.json +++ b/homeassistant/components/meater/translations/hu.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "service_unavailable_error": "Az API jelenleg nem el\u00e9rhet\u0151, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "service_unavailable_error": "Az API jelenleg nem el\u00e9rhet\u0151, k\u00e9rem, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown_auth_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index 00fff153254..7c7232d46a0 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -4,9 +4,9 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa.", - "invalid_ip": "Az IP -c\u00edm \u00e9rv\u00e9nytelen (a MAC -c\u00edmet nem siker\u00fclt meghat\u00e1rozni). K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", - "invalid_port": "A portnak 1024 \u00e9s 65535 k\u00f6z\u00f6tt kell lennie. K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra." + "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa.", + "invalid_ip": "Az IP -c\u00edm \u00e9rv\u00e9nytelen (a MAC-c\u00edmet nem siker\u00fclt meghat\u00e1rozni). K\u00e9rem, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "invalid_port": "A portsz\u00e1mnak 1024 \u00e9s 65535 k\u00f6z\u00f6tt kell lennie. K\u00e9rem, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "step": { "user": { diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index 433714de477..e1d9c10b053 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -34,7 +34,7 @@ "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si (subscribe) topik", "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3", "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", - "not_a_number": "K\u00e9rj\u00fck, adja meg a sz\u00e1mot", + "not_a_number": "K\u00e9rem, sz\u00e1mot adjon meg", "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie", "same_topic": "A feliratkoz\u00e1s \u00e9s a k\u00f6zz\u00e9t\u00e9tel t\u00e9m\u00e1i ugyanazok", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 9662d4175fd..0cb2c91d819 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}), \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Cloud Console: \n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 OAuth]({oauth_consent_url}) \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u043d\u0430 \u043d\u0435\u0439 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f.\n2. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n3. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n4. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web Application** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0442\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\n5. \u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 `{redirect_url}` \u0432 \u043f\u043e\u043b\u0435 *Authorized redirect URI*." + }, "config": { "abort": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", @@ -30,6 +33,17 @@ "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Google. \n\n\u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0442\u043e\u043a\u0435\u043d.", "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 Google" }, + "auth_upgrade": { + "description": "\u041f\u0440\u0435\u0436\u043d\u0438\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0431\u044b\u043b \u0443\u043f\u0440\u0430\u0437\u0434\u043d\u0435\u043d Google \u0434\u043b\u044f \u043f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438. \u0412\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\n\n\u0427\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u043b\u044f \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c Nest \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438]({more_info_url}).", + "title": "Nest: \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 *example-project-12345*. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 [Google Cloud Console({cloud_console_url}) \u0438\u043b\u0438 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e]({more_info_url}).", + "title": "Nest: \u0432\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID" + }, "device_project_upgrade": { "title": "Nest: \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c" }, diff --git a/homeassistant/components/netgear/translations/hu.json b/homeassistant/components/netgear/translations/hu.json index 4432e08a508..c0c472bad30 100644 --- a/homeassistant/components/netgear/translations/hu.json +++ b/homeassistant/components/netgear/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "config": "Csatlakoz\u00e1si vagy bejelentkez\u00e9si hiba: k\u00e9rj\u00fck, ellen\u0151rizze a konfigur\u00e1ci\u00f3t" + "config": "Csatlakoz\u00e1si vagy bejelentkez\u00e9si hiba: k\u00e9rem, ellen\u0151rizze a konfigur\u00e1ci\u00f3t" }, "step": { "user": { diff --git a/homeassistant/components/nfandroidtv/translations/hu.json b/homeassistant/components/nfandroidtv/translations/hu.json index 65cd2bebc01..6e83a5f7c6d 100644 --- a/homeassistant/components/nfandroidtv/translations/hu.json +++ b/homeassistant/components/nfandroidtv/translations/hu.json @@ -13,7 +13,7 @@ "host": "C\u00edm", "name": "Elnevez\u00e9s" }, - "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." + "description": "K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." } } } diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index 2c794b5cd9d..a58669fe11c 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -11,7 +11,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "Nem siker\u00fclt az OVO Energy hiteles\u00edt\u00e9se. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", + "description": "Nem siker\u00fclt az OVO Energy hiteles\u00edt\u00e9se. K\u00e9rem, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json index 245b0dc2a0c..990ecc7a561 100644 --- a/homeassistant/components/plaato/translations/hu.json +++ b/homeassistant/components/plaato/translations/hu.json @@ -20,7 +20,7 @@ "token": "Auth Token beilleszt\u00e9se ide", "use_webhook": "Webhook haszn\u00e1lata" }, - "description": "Az API lek\u00e9rdez\u00e9s\u00e9hez egy `auth_token` sz\u00fcks\u00e9ges, amelyet az [ezek] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) utas\u00edt\u00e1sok k\u00f6vet\u00e9s\u00e9vel lehet megszerezni. \n\n Kiv\u00e1lasztott eszk\u00f6z: ** {device_type} ** \n\nHa ink\u00e1bb a be\u00e9p\u00edtett webhook m\u00f3dszert haszn\u00e1lja (csak az Airlock eset\u00e9ben), k\u00e9rj\u00fck, jel\u00f6lje be az al\u00e1bbi n\u00e9gyzetet, \u00e9s hagyja \u00fcresen az Auth Token elemet", + "description": "Az API lek\u00e9rdez\u00e9s\u00e9hez egy `auth_token` sz\u00fcks\u00e9ges, amelyet az [ezek] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) utas\u00edt\u00e1sok k\u00f6vet\u00e9s\u00e9vel lehet megszerezni. \n\nKiv\u00e1lasztott eszk\u00f6z: **{device_type}** \n\nHa ink\u00e1bb a be\u00e9p\u00edtett webhook m\u00f3dszert haszn\u00e1lja (csak az Airlock eset\u00e9ben), k\u00e9rem, jel\u00f6lje be az al\u00e1bbi n\u00e9gyzetet, \u00e9s hagyja \u00fcresen az Auth Token elemet", "title": "API m\u00f3dszer kiv\u00e1laszt\u00e1sa" }, "user": { diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index 1bf9efee843..ccd3d344f39 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "anna_with_adam": "Anna i Adam detectats. Afegeix l'Adam en lloc de l'Anna" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 3f365bfa25e..cd10502d0c3 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 2d50be06193..9f2f2e0b1b6 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Teenus on juba seadistatud" + "already_configured": "Teenus on juba seadistatud", + "anna_with_adam": "Nii Anna kui ka Adam on tuvastatud. Lisa oma Anna asemel oma Adam" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 0306437b405..85f4f652c18 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "anna_with_adam": "Anna et Adam ont tous deux \u00e9t\u00e9 d\u00e9tect\u00e9s. Ajoutez votre Adam au lieu de votre Anna" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index b622109797c..b11137a2658 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -29,7 +29,7 @@ "port": "Port", "username": "Smile Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg", + "description": "K\u00e9rem, adja meg", "title": "Csatlakoz\u00e1s a Smile-hoz" } } diff --git a/homeassistant/components/plugwise/translations/pt-BR.json b/homeassistant/components/plugwise/translations/pt-BR.json index 12f8070f074..5f667f8d6ff 100644 --- a/homeassistant/components/plugwise/translations/pt-BR.json +++ b/homeassistant/components/plugwise/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "anna_with_adam": "Tanto Anna quanto Adam detectaram. Adicione seu Adam em vez de sua Anna" }, "error": { "cannot_connect": "Falha ao conectar", diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 62b7c1e27d3..c7cfa232fd0 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -13,7 +13,11 @@ "step": { "user": { "data": { - "flow_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + "flow_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "Smile ID", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Smile" }, "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:", "title": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Plugwise" diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index 9d37a79462b..2ea2c4b09ba 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "anna_with_adam": "\u767c\u73fe Anna \u8207 Adam\uff0c\u662f\u5426\u8981\u65b0\u589e Adam \u800c\u975e Anna" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index 6f53b1ef575..2f59abef956 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -9,7 +9,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "wrong_version": "Az powerwall nem t\u00e1mogatott szoftververzi\u00f3t haszn\u00e1l. K\u00e9rj\u00fck, fontolja meg a probl\u00e9ma friss\u00edt\u00e9s\u00e9t vagy jelent\u00e9s\u00e9t, hogy megoldhat\u00f3 legyen." + "wrong_version": "Az powerwall nem t\u00e1mogatott szoftververzi\u00f3t haszn\u00e1l. K\u00e9rem, fontolja meg a probl\u00e9ma friss\u00edt\u00e9s\u00e9t vagy jelent\u00e9s\u00e9t, hogy megoldhat\u00f3 legyen." }, "flow_title": "{name} ({ip_address})", "step": { diff --git a/homeassistant/components/radiotherm/translations/ru.json b/homeassistant/components/radiotherm/translations/ru.json new file mode 100644 index 00000000000..daac6c7c5ae --- /dev/null +++ b/homeassistant/components/radiotherm/translations/ru.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0433\u043e \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f \u043f\u0440\u0438 \u0440\u0435\u0433\u0443\u043b\u0438\u0440\u043e\u0432\u043a\u0435 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/hu.json b/homeassistant/components/renault/translations/hu.json index d74d8cdf9e4..9d2367af80b 100644 --- a/homeassistant/components/renault/translations/hu.json +++ b/homeassistant/components/renault/translations/hu.json @@ -19,7 +19,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, friss\u00edtse {username} jelszav\u00e1t", + "description": "K\u00e9rem, friss\u00edtse {username} jelszav\u00e1t", "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li hiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/rhasspy/translations/it.json b/homeassistant/components/rhasspy/translations/it.json new file mode 100644 index 00000000000..bd2daba89f6 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/it.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Vuoi abilitare il supporto Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index e109b6e7043..b4126e202d1 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -19,7 +19,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rni az eszk\u00f6zr\u0151l. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", + "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rni az eszk\u00f6zr\u0151l. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", "title": "Jelsz\u00f3 megad\u00e1sa" }, "manual": { diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index aa536aeeddd..cf5d1609dd3 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -13,7 +13,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "Nem siker\u00fclt felfedezni a Roon-kiszolg\u00e1l\u00f3t. K\u00e9rj\u00fck, adja meg g\u00e9p c\u00edm\u00e9t \u00e9s portj\u00e1t." + "description": "Nem siker\u00fclt felfedezni a Roon-kiszolg\u00e1l\u00f3t. K\u00e9rem, adja meg g\u00e9p c\u00edm\u00e9t \u00e9s portj\u00e1t." }, "link": { "description": "Enged\u00e9lyeznie kell az Home Assistantot a Roonban. Miut\u00e1n r\u00e1kattintott a Mehet gombra, nyissa meg a Roon Core alkalmaz\u00e1st, nyissa meg a Be\u00e1ll\u00edt\u00e1sokat, \u00e9s enged\u00e9lyezze a Home Assistant funkci\u00f3t a B\u0151v\u00edtm\u00e9nyek lapon.", diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index b53f50ff74a..d4f37d72922 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -12,7 +12,7 @@ }, "error": { "auth_missing": "Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV be\u00e1ll\u00edt\u00e1sait Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", - "invalid_pin": "A PIN k\u00f3d \u00e9rv\u00e9nytelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra." + "invalid_pin": "A PIN k\u00f3d \u00e9rv\u00e9nytelen, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra." }, "flow_title": "{device}", "step": { @@ -20,7 +20,7 @@ "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {device} k\u00e9sz\u00fcl\u00e9k\u00e9t? Ha kor\u00e1bban m\u00e9g sosem csatlakoztatta Home Assistanthoz, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r." }, "encrypted_pairing": { - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" + "description": "K\u00e9rem, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" }, "pairing": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {device} k\u00e9sz\u00fcl\u00e9k\u00e9t? Ha kor\u00e1bban m\u00e9g sosem csatlakoztatta Home Assistanthoz, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r." @@ -29,7 +29,7 @@ "description": "A bek\u00fcld\u00e9s ut\u00e1n, fogadja el a {device} felugr\u00f3 ablak\u00e1ban l\u00e1that\u00f3 \u00fczenetet, mely 30 m\u00e1sodpercig \u00e1ll rendelkez\u00e9sre, vagy adja meg a PIN k\u00f3dot." }, "reauth_confirm_encrypted": { - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" + "description": "K\u00e9rem, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" }, "user": { "data": { diff --git a/homeassistant/components/scrape/translations/ru.json b/homeassistant/components/scrape/translations/ru.json new file mode 100644 index 00000000000..2d014592e85 --- /dev/null +++ b/homeassistant/components/scrape/translations/ru.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", + "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "device_class": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", + "index": "\u0418\u043d\u0434\u0435\u043a\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", + "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c", + "state_class": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "attribute": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0435\u0433\u0430.", + "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", + "device_class": "\u0422\u0438\u043f/\u043a\u043b\u0430\u0441\u0441 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435.", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", + "index": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0438\u0437 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u043c CSS \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.", + "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "select": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0442\u0435\u0433 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043a\u0430\u0442\u044c. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u0432 CSS Beautifulsoup.", + "state_class": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 state_class \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", + "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430.", + "verify_ssl": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442/\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 SSL/TLS. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441\u0430\u043c\u043e\u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", + "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "device_class": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", + "index": "\u0418\u043d\u0434\u0435\u043a\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", + "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c", + "state_class": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "attribute": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0435\u0433\u0430.", + "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", + "device_class": "\u0422\u0438\u043f/\u043a\u043b\u0430\u0441\u0441 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435.", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", + "index": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0438\u0437 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u043c CSS \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.", + "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "select": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0442\u0435\u0433 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043a\u0430\u0442\u044c. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u0432 CSS Beautifulsoup.", + "state_class": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 state_class \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", + "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430.", + "verify_ssl": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442/\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 SSL/TLS. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441\u0430\u043c\u043e\u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.ru.json b/homeassistant/components/sensibo/translations/sensor.ru.json new file mode 100644 index 00000000000..c916a2b22f4 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.ru.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439", + "s": "\u0427\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 18f53ca232d..71a9e96126b 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "firmware_not_fully_provisioned": "Az eszk\u00f6z nincs teljesen be\u00fczemelve. K\u00e9rj\u00fck, vegye fel a kapcsolatot a Shelly \u00fcgyf\u00e9lszolg\u00e1lat\u00e1val", + "firmware_not_fully_provisioned": "Az eszk\u00f6z be\u00fczemel\u00e9se nem teljes. K\u00e9rem, vegye fel a kapcsolatot a Shelly \u00fcgyf\u00e9lszolg\u00e1lat\u00e1val", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/sia/translations/hu.json b/homeassistant/components/sia/translations/hu.json index 63a5208a44c..cd3bf4a9578 100644 --- a/homeassistant/components/sia/translations/hu.json +++ b/homeassistant/components/sia/translations/hu.json @@ -1,9 +1,9 @@ { "config": { "error": { - "invalid_account_format": "A sz\u00e1mla nem hexa\u00e9rt\u00e9k, k\u00e9rj\u00fck, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", + "invalid_account_format": "A megadott fi\u00f3k nem hexa\u00e9rt\u00e9k, k\u00e9rem, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", "invalid_account_length": "A fi\u00f3k nem megfelel\u0151 hossz\u00fas\u00e1g\u00fa, 3 \u00e9s 16 karakter k\u00f6z\u00f6tt kell lennie.", - "invalid_key_format": "A kulcs nem hexa\u00e9rt\u00e9k, k\u00e9rj\u00fck, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", + "invalid_key_format": "A megadott kulcs nem hexa\u00e9rt\u00e9k, k\u00e9rem, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", "invalid_key_length": "A kulcs nem megfelel\u0151 hossz\u00fas\u00e1g\u00fa, 16, 24 vagy 32 hexa karakterb\u0151l kell \u00e1llnia.", "invalid_ping": "A ping intervallumnak 1 \u00e9s 1440 perc k\u00f6z\u00f6tt kell lennie.", "invalid_zones": "Legal\u00e1bb 1 z\u00f3n\u00e1nak kell lennie.", diff --git a/homeassistant/components/skybell/translations/ru.json b/homeassistant/components/skybell/translations/ru.json new file mode 100644 index 00000000000..8c30e7c8ad6 --- /dev/null +++ b/homeassistant/components/skybell/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 712cbd0951d..f1e0e5012ad 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_configured_local_device": "A helyi eszk\u00f6z\u00f6k m\u00e1r konfigur\u00e1lva vannak. K\u00e9rj\u00fck, el\u0151sz\u00f6r t\u00e1vol\u00edtsa el ezeket, miel\u0151tt konfigur\u00e1lja a felh\u0151alap\u00fa eszk\u00f6zt.", + "already_configured_local_device": "A helyi eszk\u00f6z\u00f6k m\u00e1r konfigur\u00e1lva vannak. K\u00e9rem, el\u0151sz\u00f6r t\u00e1vol\u00edtsa el ezeket a rendszerb\u0151l, miel\u0151tt bekonfigur\u00e1lja \u0151ket felh\u0151 alapon.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_mdns": "Nem t\u00e1mogatott eszk\u00f6z a Smappee integr\u00e1ci\u00f3hoz.", diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index 90ea748ae33..47698ef528c 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant nincs megfelel\u0151en konfigur\u00e1lva a SmartThings friss\u00edt\u00e9seinek fogad\u00e1s\u00e1ra. A webhook URL \u00e9rv\u00e9nytelen:\n > {webhook_url} \n\nK\u00e9rj\u00fck, friss\u00edtse konfigur\u00e1ci\u00f3j\u00e1t az [utas\u00edt\u00e1sok]({component_url}) szerint, ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "invalid_webhook_url": "Home Assistant nincs megfelel\u0151en konfigur\u00e1lva a SmartThings friss\u00edt\u00e9seinek fogad\u00e1s\u00e1ra. A webhook URL \u00e9rv\u00e9nytelen:\n > {webhook_url} \n\nK\u00e9rem, friss\u00edtse konfigur\u00e1ci\u00f3j\u00e1t az [utas\u00edt\u00e1sok]({component_url}) szerint, ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", "no_available_locations": "Nincsenek be\u00e1ll\u00edthat\u00f3 SmartThings helyek a Home Assistant alkalmaz\u00e1sban." }, "error": { @@ -9,7 +9,7 @@ "token_forbidden": "A token nem rendelkezik a sz\u00fcks\u00e9ges OAuth-tartom\u00e1nyokkal.", "token_invalid_format": "A tokennek UID / GUID form\u00e1tumban kell lennie", "token_unauthorized": "A token \u00e9rv\u00e9nytelen vagy m\u00e1r nem enged\u00e9lyezett.", - "webhook_error": "SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a webhook URL-t. K\u00e9rj\u00fck, ellen\u0151rizze, hogy a webhook URL el\u00e9rhet\u0151-e az internet fel\u0151l, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra." + "webhook_error": "SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a webhook URL-t. K\u00e9rem, ellen\u0151rizze, hogy a webhook URL el\u00e9rhet\u0151-e az internet fel\u0151l, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra." }, "step": { "authorize": { @@ -19,14 +19,14 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban.", + "description": "K\u00e9rem, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban.", "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent" }, "select_location": { "data": { "location_id": "Elhelyezked\u00e9s" }, - "description": "K\u00e9rj\u00fck, v\u00e1lassza ki azt a SmartThings helyet, amelyet hozz\u00e1 szeretne adni a Home Assistant szolg\u00e1ltat\u00e1shoz. Ezut\u00e1n \u00faj ablakot nyitunk, \u00e9s megk\u00e9rj\u00fck, hogy jelentkezzen be, \u00e9s enged\u00e9lyezze a Home Assistant integr\u00e1ci\u00f3j\u00e1nak telep\u00edt\u00e9s\u00e9t a kiv\u00e1lasztott helyre.", + "description": "K\u00e9rem, v\u00e1lassza ki azt a SmartThings helyet, amelyet hozz\u00e1 szeretne adni a Home Assistant szolg\u00e1ltat\u00e1shoz. Ezut\u00e1n az \u00faj ablakban jelentkezzen be, \u00e9s enged\u00e9lyezze a Home Assistant integr\u00e1ci\u00f3j\u00e1nak telep\u00edt\u00e9s\u00e9t a kiv\u00e1lasztott helyre.", "title": "Hely kiv\u00e1laszt\u00e1sa" }, "user": { diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json index 89194a7b204..3cd6dff8c2e 100644 --- a/homeassistant/components/soma/translations/hu.json +++ b/homeassistant/components/soma/translations/hu.json @@ -4,7 +4,7 @@ "already_setup": "Csak egy Soma-fi\u00f3k konfigur\u00e1lhat\u00f3.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "connection_error": "Nem siker\u00fclt csatlakozni.", - "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A Soma komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "result_error": "A SOMA Connect hiba\u00e1llapottal v\u00e1laszolt." }, "create_entry": { @@ -16,7 +16,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", + "description": "K\u00e9rem, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", "title": "SOMA Connect" } } diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index f387ff6bb3a..735c4942fb2 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "reauth_account_mismatch": "A Spotify-fi\u00f3kkal hiteles\u00edtett fi\u00f3k nem egyezik meg az \u00faj hiteles\u00edt\u00e9shez sz\u00fcks\u00e9ges fi\u00f3kkal." }, diff --git a/homeassistant/components/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json index a7d6c6a7de4..87e78abedb4 100644 --- a/homeassistant/components/steam_online/translations/hu.json +++ b/homeassistant/components/steam_online/translations/hu.json @@ -26,7 +26,7 @@ }, "options": { "error": { - "unauthorized": "Bar\u00e1ti lista korl\u00e1tozott: K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t arr\u00f3l, hogyan l\u00e1thatja az \u00f6sszes t\u00f6bbi bar\u00e1tot." + "unauthorized": "A bar\u00e1t-lista korl\u00e1tozva van: K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t arr\u00f3l, hogyan l\u00e1thatja az \u00f6sszes t\u00f6bbi bar\u00e1tj\u00e1t." }, "step": { "init": { diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index 0da65eda473..b3d84b7f9e6 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0440\u0443\u0437\u0435\u0439 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0432\u0441\u0435\u0445 \u0434\u0440\u0443\u0437\u044c\u044f\u0445." + }, "step": { "init": { "data": { diff --git a/homeassistant/components/subaru/translations/hu.json b/homeassistant/components/subaru/translations/hu.json index 42f9f6bb2c9..9194b4e5044 100644 --- a/homeassistant/components/subaru/translations/hu.json +++ b/homeassistant/components/subaru/translations/hu.json @@ -11,14 +11,14 @@ "incorrect_pin": "Helytelen PIN", "incorrect_validation_code": "Helytelen \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3d", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "two_factor_request_failed": "A k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3d lek\u00e9r\u00e9se sikertelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra" + "two_factor_request_failed": "A k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3d lek\u00e9r\u00e9se sikertelen, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra." }, "step": { "pin": { "data": { "pin": "PIN" }, - "description": "K\u00e9rj\u00fck, adja meg MySubaru PIN-k\u00f3dj\u00e1t\n MEGJEGYZ\u00c9S: A sz\u00e1ml\u00e1n szerepl\u0151 \u00f6sszes j\u00e1rm\u0171nek azonos PIN-k\u00f3ddal kell rendelkeznie", + "description": "K\u00e9rem, adja meg MySubaru PIN-k\u00f3dj\u00e1t\nMEGJEGYZ\u00c9S: A fi\u00f3kban szerepl\u0151 \u00f6sszes j\u00e1rm\u0171nek azonos PIN-k\u00f3ddal kell rendelkeznie", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" }, "two_factor": { @@ -32,7 +32,7 @@ "data": { "validation_code": "\u00c9rv\u00e9nyes\u00edt\u00e9si k\u00f3d" }, - "description": "K\u00e9rj\u00fck, adja meg az \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3dot", + "description": "K\u00e9rem, adja meg az \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3dot", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" }, "user": { @@ -41,7 +41,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg MySubaru hiteles\u00edt\u0151 adatait\n MEGJEGYZ\u00c9S: A kezdeti be\u00e1ll\u00edt\u00e1s ak\u00e1r 30 m\u00e1sodpercet is ig\u00e9nybe vehetnek", + "description": "K\u00e9rem, adja meg MySubaru hiteles\u00edt\u0151 adatait.\nMEGJEGYZ\u00c9S: A kezdeti be\u00e1ll\u00edt\u00e1s ak\u00e1r 30 m\u00e1sodpercet is ig\u00e9nybe vehetnek.", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" } } diff --git a/homeassistant/components/system_bridge/translations/hu.json b/homeassistant/components/system_bridge/translations/hu.json index c570cd2e3c2..0671bcce90b 100644 --- a/homeassistant/components/system_bridge/translations/hu.json +++ b/homeassistant/components/system_bridge/translations/hu.json @@ -24,7 +24,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg kapcsolati adatait." + "description": "K\u00e9rem, adja meg kapcsolati adatait." } } } diff --git a/homeassistant/components/tankerkoenig/translations/ru.json b/homeassistant/components/tankerkoenig/translations/ru.json index bf61fddc0c5..d2b34eb264b 100644 --- a/homeassistant/components/tankerkoenig/translations/ru.json +++ b/homeassistant/components/tankerkoenig/translations/ru.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "no_stations": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u043d\u0438 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u0432 \u0440\u0430\u0434\u0438\u0443\u0441\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "select_station": { "data": { "stations": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438" @@ -32,7 +38,8 @@ "init": { "data": { "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f", - "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", + "stations": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Tankerkoenig" } diff --git a/homeassistant/components/tomorrowio/translations/hu.json b/homeassistant/components/tomorrowio/translations/hu.json index d619b6346a4..4d906cd1e03 100644 --- a/homeassistant/components/tomorrowio/translations/hu.json +++ b/homeassistant/components/tomorrowio/translations/hu.json @@ -3,7 +3,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", - "rate_limited": "Jelenleg korl\u00e1tozott a hozz\u00e1f\u00e9r\u00e9s, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "rate_limited": "Jelenleg korl\u00e1tozott a hozz\u00e1f\u00e9r\u00e9s, k\u00e9rem, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index ee82564033c..ac79e28c1b0 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0431\u0430\u0439\u043f\u0430\u0441 \u043f\u0440\u0438 \u043d\u0438\u0437\u043a\u043e\u043c \u0437\u0430\u0440\u044f\u0434\u0435 \u0431\u0430\u0442\u0430\u0440\u0435\u0438" + }, + "description": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u0437\u043e\u043d\u044b, \u043a\u043e\u0433\u0434\u0430 \u043e\u043d\u0438 \u0441\u043e\u043e\u0431\u0449\u0430\u044e\u0442 \u043e \u0440\u0430\u0437\u0440\u044f\u0434\u0435 \u0431\u0430\u0442\u0430\u0440\u0435\u0438.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/hu.json b/homeassistant/components/tractive/translations/hu.json index d0f75a28ed0..5b1d9a35512 100644 --- a/homeassistant/components/tractive/translations/hu.json +++ b/homeassistant/components/tractive/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt" }, "error": { diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index 1d0fff95c3a..8ea65342e78 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "discovery_started": "Rilevamento " + "discovery_started": "Rilevamento iniziato" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/uptimerobot/translations/hu.json b/homeassistant/components/uptimerobot/translations/hu.json index d17c9bf8ac1..4a607c03303 100644 --- a/homeassistant/components/uptimerobot/translations/hu.json +++ b/homeassistant/components/uptimerobot/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt", "unknown": "V\u00e1ratlan hiba" }, diff --git a/homeassistant/components/verisure/translations/hu.json b/homeassistant/components/verisure/translations/hu.json index 324033b666a..fbb40f2325a 100644 --- a/homeassistant/components/verisure/translations/hu.json +++ b/homeassistant/components/verisure/translations/hu.json @@ -6,14 +6,21 @@ }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unknown_mfa": "Ismeretlen hiba t\u00f6rt\u00e9nt a be\u00e1ll\u00edt\u00e1s sor\u00e1n" }, "step": { "installation": { "data": { "giid": "Telep\u00edt\u00e9s" }, - "description": "Home Assistant t\u00f6bb Verisure telep\u00edt\u00e9st tal\u00e1lt a Saj\u00e1t oldalak fi\u00f3kj\u00e1ban. K\u00e9rj\u00fck, v\u00e1lassza ki azt a telep\u00edt\u00e9st, amelyet hozz\u00e1 k\u00edv\u00e1nja adni a Home Assistant p\u00e9ld\u00e1ny\u00e1hoz." + "description": "Home Assistant t\u00f6bb Verisure telep\u00edt\u00e9st tal\u00e1lt a Saj\u00e1t oldalak fi\u00f3kj\u00e1ban. K\u00e9rem, v\u00e1lassza ki azt a telep\u00edt\u00e9st, amelyet hozz\u00e1 k\u00edv\u00e1nja adni a Home Assistant p\u00e9ld\u00e1ny\u00e1hoz." + }, + "mfa": { + "data": { + "code": "Ellen\u0151rz\u0151 k\u00f3d", + "description": "A fi\u00f3kj\u00e1ban be van \u00e1ll\u00edtva a 2 l\u00e9pcs\u0151s ellen\u0151rz\u00e9s. K\u00e9rem, adja meg a Verisure \u00e1ltal k\u00fcld\u00f6tt ellen\u0151rz\u0151 k\u00f3dot." + } }, "reauth_confirm": { "data": { @@ -22,6 +29,12 @@ "password": "Jelsz\u00f3" } }, + "reauth_mfa": { + "data": { + "code": "Ellen\u0151rz\u0151 k\u00f3d", + "description": "A fi\u00f3kj\u00e1ban be van \u00e1ll\u00edtva a 2 l\u00e9pcs\u0151s ellen\u0151rz\u00e9s. K\u00e9rem, adja meg a Verisure \u00e1ltal k\u00fcld\u00f6tt ellen\u0151rz\u0151 k\u00f3dot." + } + }, "user": { "data": { "description": "Jelentkezzen be a Verisure My Pages fi\u00f3kj\u00e1val.", diff --git a/homeassistant/components/verisure/translations/it.json b/homeassistant/components/verisure/translations/it.json index e30976a37a6..b913a47af03 100644 --- a/homeassistant/components/verisure/translations/it.json +++ b/homeassistant/components/verisure/translations/it.json @@ -15,6 +15,12 @@ }, "description": "Home Assistant ha trovato pi\u00f9 installazioni Verisure nel tuo account My Pages. Per favore, seleziona l'installazione da aggiungere a Home Assistant." }, + "mfa": { + "data": { + "code": "Codice di verifica", + "description": "Il tuo account ha la verifica in due passaggi abilitata. Inserisci il codice di verifica che ti viene inviato da Verisure." + } + }, "reauth_confirm": { "data": { "description": "Autenticati nuovamente con il tuo account Verisure My Pages.", @@ -22,6 +28,11 @@ "password": "Password" } }, + "reauth_mfa": { + "data": { + "code": "Codice di verifica" + } + }, "user": { "data": { "description": "Accedi con il tuo account Verisure My Pages.", diff --git a/homeassistant/components/vlc_telnet/translations/hu.json b/homeassistant/components/vlc_telnet/translations/hu.json index bac42b4d4c3..b1247a74d2e 100644 --- a/homeassistant/components/vlc_telnet/translations/hu.json +++ b/homeassistant/components/vlc_telnet/translations/hu.json @@ -21,7 +21,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg a helyes jelsz\u00f3t: {host}" + "description": "K\u00e9rem, adja meg a helyes jelsz\u00f3t: {host}" }, "user": { "data": { diff --git a/homeassistant/components/vulcan/translations/hu.json b/homeassistant/components/vulcan/translations/hu.json index 65d90d6e24a..953eb90146f 100644 --- a/homeassistant/components/vulcan/translations/hu.json +++ b/homeassistant/components/vulcan/translations/hu.json @@ -3,13 +3,13 @@ "abort": { "all_student_already_configured": "M\u00e1r minden tanul\u00f3 hozz\u00e1adva.", "already_configured": "Ezt a tanul\u00f3t m\u00e1r hozz\u00e1adt\u00e1k.", - "no_matching_entries": "Nem tal\u00e1ltunk megfelel\u0151 bejegyz\u00e9st, k\u00e9rj\u00fck, haszn\u00e1ljon m\u00e1sik fi\u00f3kot, vagy t\u00e1vol\u00edtsa el az elavult di\u00e1kkal val\u00f3 integr\u00e1ci\u00f3t...", + "no_matching_entries": "Nem tal\u00e1lhat\u00f3 megfelel\u0151 bejegyz\u00e9s, k\u00e9rem, haszn\u00e1ljon m\u00e1sik fi\u00f3kot, vagy t\u00e1vol\u00edtsa el az elavult di\u00e1kkal val\u00f3 integr\u00e1ci\u00f3t...", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres" }, "error": { "cannot_connect": "Csatlakoz\u00e1si hiba \u2013 ellen\u0151rizze az internetkapcsolatot", - "expired_credentials": "Lej\u00e1rt hiteles\u00edt\u0151 adatok - k\u00e9rj\u00fck, hozzon l\u00e9tre \u00fajakat a Vulcan mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n.", - "expired_token": "Lej\u00e1rt token \u2013 k\u00e9rj\u00fck, hozzon l\u00e9tre egy \u00fajat", + "expired_credentials": "Lej\u00e1rt hiteles\u00edt\u0151 adatok - k\u00e9rem, hozzon l\u00e9tre \u00fajakat a Vulcan mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n.", + "expired_token": "Lej\u00e1rt token \u2013 k\u00e9rem, hozzon l\u00e9tre egy \u00fajat", "invalid_pin": "\u00c9rv\u00e9nytelen PIN-k\u00f3d", "invalid_symbol": "\u00c9rv\u00e9nytelen szimb\u00f3lum", "invalid_token": "\u00c9rv\u00e9nytelen token", diff --git a/homeassistant/components/webostv/translations/hu.json b/homeassistant/components/webostv/translations/hu.json index de3f59d8c90..b4ef5f39aa1 100644 --- a/homeassistant/components/webostv/translations/hu.json +++ b/homeassistant/components/webostv/translations/hu.json @@ -6,7 +6,7 @@ "error_pairing": "Csatlakozva az LG webOS TV-hez, de a p\u00e1ros\u00edt\u00e1s nem siker\u00fclt" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, kapcsolja be a TV-t vagy ellen\u0151rizze az ip-c\u00edmet." + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rem, kapcsolja be a TV-t vagy ellen\u0151rizze az ip-c\u00edmet." }, "flow_title": "LG webOS Smart TV", "step": { diff --git a/homeassistant/components/wiz/translations/hu.json b/homeassistant/components/wiz/translations/hu.json index 0c27ee730b3..90c3b3b35a2 100644 --- a/homeassistant/components/wiz/translations/hu.json +++ b/homeassistant/components/wiz/translations/hu.json @@ -6,7 +6,7 @@ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { - "bulb_time_out": "Nem lehet csatlakoztatni az izz\u00f3hoz. Lehet, hogy az izz\u00f3 offline \u00e1llapotban van, vagy rossz IP-t adott meg. K\u00e9rj\u00fck, kapcsolja fel a l\u00e1mp\u00e1t, \u00e9s pr\u00f3b\u00e1lja \u00fajra!", + "bulb_time_out": "Nem lehet csatlakoztatni az izz\u00f3hoz. Lehet, hogy az izz\u00f3 offline \u00e1llapotban van, vagy rossz IP-t adott meg. K\u00e9rem, kapcsolja fel a l\u00e1mp\u00e1t, \u00e9s pr\u00f3b\u00e1lja \u00fajra!", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "no_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm.", "no_wiz_light": "Az izz\u00f3 nem csatlakoztathat\u00f3 a WiZ Platform integr\u00e1ci\u00f3n kereszt\u00fcl.", diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 51f093b871c..a7536283240 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -9,7 +9,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rj\u00fck, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got", + "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rem, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got", "cloud_login_error": "Nem siker\u00fclt bejelentkezni a Xioami Miio Cloud szolg\u00e1ltat\u00e1sba, ellen\u0151rizze a hiteles\u00edt\u0151 adatokat.", "cloud_no_devices": "Nincs eszk\u00f6z ebben a Xiaomi Miio felh\u0151fi\u00f3kban.", "unknown_device": "Az eszk\u00f6z modell nem ismert, nem tudja be\u00e1ll\u00edtani az eszk\u00f6zt a konfigur\u00e1ci\u00f3s folyamat seg\u00edts\u00e9g\u00e9vel.", @@ -52,7 +52,7 @@ }, "options": { "error": { - "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rj\u00fck, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got" + "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rem, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got" }, "step": { "init": { diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 37e19e5471f..f93ff278c5f 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -100,7 +100,7 @@ "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Nem siker\u00fclt csatlakozni", - "different_device": "A csatlakoztatott USB-eszk\u00f6z nem ugyanaz, mint amelyet kor\u00e1bban ehhez a konfigur\u00e1ci\u00f3s bejegyz\u00e9shez konfigur\u00e1ltak. K\u00e9rj\u00fck, ink\u00e1bb hozzon l\u00e9tre egy \u00faj konfigur\u00e1ci\u00f3s bejegyz\u00e9st az \u00faj eszk\u00f6zh\u00f6z." + "different_device": "A csatlakoztatott USB-eszk\u00f6z nem ugyanaz, mint amelyet kor\u00e1bban ehhez a konfigur\u00e1ci\u00f3s bejegyz\u00e9shez konfigur\u00e1ltak. K\u00e9rem, ink\u00e1bb hozzon l\u00e9tre egy \u00faj konfigur\u00e1ci\u00f3s bejegyz\u00e9st az \u00faj eszk\u00f6zh\u00f6z." }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", From 46551a50eff5c20ed9cd99b0eeea1ead15de108a Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Thu, 21 Jul 2022 03:04:04 -0400 Subject: [PATCH 605/820] Bump pymazda to 0.3.7 (#75546) --- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index acf5282689f..d521bc748e0 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.3.6"], + "requirements": ["pymazda==0.3.7"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 6e525274af2..26d7c6bce14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1645,7 +1645,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.6 +pymazda==0.3.7 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf07a07117e..48826ebdf1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1124,7 +1124,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.6 +pymazda==0.3.7 # homeassistant.components.melcloud pymelcloud==2.5.6 From c861259749e8a6f421b5bfdf63afeb03974a447c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 21 Jul 2022 10:56:53 +0200 Subject: [PATCH 606/820] Hide inactive repairs issues (#75556) --- .../components/repairs/websocket_api.py | 1 + .../components/repairs/test_websocket_api.py | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 2e4e166f3e9..b6a71773273 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -73,6 +73,7 @@ def ws_list_issues( issues = [ dataclasses.asdict(issue, dict_factory=ws_dict) for issue in issue_registry.issues.values() + if issue.active ] connection.send_result(msg["id"], {"issues": issues}) diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 388912e0adc..73d1898fcb7 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -9,7 +9,11 @@ import pytest import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.repairs import RepairsFlow, async_create_issue +from homeassistant.components.repairs import ( + RepairsFlow, + async_create_issue, + issue_registry, +) from homeassistant.components.repairs.const import DOMAIN from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant @@ -366,8 +370,24 @@ async def test_step_unauth( @freeze_time("2022-07-19 07:53:05") -async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: +async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> None: """Test we can list issues.""" + + # Add an inactive issue, this should not be exposed in the list + hass_storage[issue_registry.STORAGE_KEY] = { + "version": issue_registry.STORAGE_VERSION, + "data": { + "issues": [ + { + "created": "2022-07-19T09:41:13.746514+00:00", + "dismissed_version": None, + "domain": "test", + "issue_id": "issue_3_inactive", + }, + ] + }, + } + assert await async_setup_component(hass, DOMAIN, {}) client = await hass_ws_client(hass) From baeb55e313074e545e7d9e55ae0a9cbdb2c3e571 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 21 Jul 2022 11:11:51 +0200 Subject: [PATCH 607/820] Add sync methods for create/deleting issues in repairs (#75557) --- homeassistant/components/repairs/__init__.py | 9 ++- .../components/repairs/issue_handler.py | 42 +++++++++++ tests/components/repairs/test_init.py | 73 ++++++++++++++++++- 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index f9a478514aa..4471def0dcd 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -6,13 +6,20 @@ from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api from .const import DOMAIN -from .issue_handler import async_create_issue, async_delete_issue +from .issue_handler import ( + async_create_issue, + async_delete_issue, + create_issue, + delete_issue, +) from .issue_registry import async_load as async_load_issue_registry from .models import IssueSeverity, RepairsFlow __all__ = [ "async_create_issue", "async_delete_issue", + "create_issue", + "delete_issue", "DOMAIN", "IssueSeverity", "RepairsFlow", diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index ff23c1c70a4..8eff4ac64fe 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -1,6 +1,7 @@ """The repairs integration.""" from __future__ import annotations +import functools as ft from typing import Any from awesomeversion import AwesomeVersion, AwesomeVersionStrategy @@ -11,6 +12,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) +from homeassistant.util.async_ import run_callback_threadsafe from .const import DOMAIN from .issue_registry import async_get as async_get_issue_registry @@ -113,6 +115,36 @@ def async_create_issue( ) +def create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + is_fixable: bool, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + return run_callback_threadsafe( + hass.loop, + ft.partial( + async_create_issue, + hass, + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ), + ).result() + + @callback def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: """Delete an issue. @@ -123,6 +155,16 @@ def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: issue_registry.async_delete(domain, issue_id) +def delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + return run_callback_threadsafe( + hass.loop, async_delete_issue, hass, domain, issue_id + ).result() + + @callback def async_ignore_issue( hass: HomeAssistant, domain: str, issue_id: str, ignore: bool diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index fbf240e740d..2f82a084968 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -1,15 +1,23 @@ """Test the repairs websocket API.""" +from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock, Mock +from aiohttp import ClientWebSocketResponse from freezegun import freeze_time import pytest -from homeassistant.components.repairs import async_create_issue, async_delete_issue +from homeassistant.components.repairs import ( + async_create_issue, + async_delete_issue, + create_issue, + delete_issue, +) from homeassistant.components.repairs.const import DOMAIN from homeassistant.components.repairs.issue_handler import ( async_ignore_issue, async_process_repairs_platforms, ) +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -446,3 +454,66 @@ async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> No await async_process_repairs_platforms(hass) assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"] + + +@freeze_time("2022-07-21 08:22:00") +async def test_sync_methods( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test sync method for creating and deleting an issue.""" + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + def _create_issue() -> None: + create_issue( + hass, + "fake_integration", + "sync_issue", + breaks_in_ha_version="2022.9", + is_fixable=True, + learn_more_url="https://theuselessweb.com", + severity=IssueSeverity.ERROR, + translation_key="abc_123", + translation_placeholders={"abc": "123"}, + ) + + await hass.async_add_executor_job(_create_issue) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2022.9", + "created": "2022-07-21T08:22:00+00:00", + "dismissed_version": None, + "domain": "fake_integration", + "ignored": False, + "is_fixable": True, + "issue_id": "sync_issue", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + } + ] + } + + await hass.async_add_executor_job( + delete_issue, hass, "fake_integration", "sync_issue" + ) + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} From 41f63839571643b09b0cd7ea55375ea6c4f3abe9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 21 Jul 2022 12:24:29 +0200 Subject: [PATCH 608/820] Update icons for breaking changes in MDI 7.0.96 (#75560) --- homeassistant/components/huawei_lte/sensor.py | 16 ++++++++-------- .../components/icloud/device_tracker.py | 2 +- homeassistant/components/netgear/const.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 44d03677ebd..c4cce70cbb7 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -299,7 +299,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ) ), (KEY_MONITORING_CHECK_NOTIFICATIONS, "UnreadMessage"): SensorMeta( - name="SMS unread", icon="mdi:email-receive" + name="SMS unread", icon="mdi:email-arrow-left" ), KEY_MONITORING_MONTH_STATISTICS: SensorMeta( exclude=re.compile(r"^month(duration|lastcleartime)$", re.IGNORECASE) @@ -450,7 +450,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_SMS_SMS_COUNT, "LocalDraft"): SensorMeta( name="SMS drafts (device)", - icon="mdi:email-send-outline", + icon="mdi:email-arrow-right-outline", ), (KEY_SMS_SMS_COUNT, "LocalInbox"): SensorMeta( name="SMS inbox (device)", @@ -462,15 +462,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_SMS_SMS_COUNT, "LocalOutbox"): SensorMeta( name="SMS outbox (device)", - icon="mdi:email-send", + icon="mdi:email-arrow-right", ), (KEY_SMS_SMS_COUNT, "LocalUnread"): SensorMeta( name="SMS unread (device)", - icon="mdi:email-receive", + icon="mdi:email-arrow-left", ), (KEY_SMS_SMS_COUNT, "SimDraft"): SensorMeta( name="SMS drafts (SIM)", - icon="mdi:email-send-outline", + icon="mdi:email-arrow-right-outline", ), (KEY_SMS_SMS_COUNT, "SimInbox"): SensorMeta( name="SMS inbox (SIM)", @@ -482,15 +482,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_SMS_SMS_COUNT, "SimOutbox"): SensorMeta( name="SMS outbox (SIM)", - icon="mdi:email-send", + icon="mdi:email-arrow-right", ), (KEY_SMS_SMS_COUNT, "SimUnread"): SensorMeta( name="SMS unread (SIM)", - icon="mdi:email-receive", + icon="mdi:email-arrow-left", ), (KEY_SMS_SMS_COUNT, "SimUsed"): SensorMeta( name="SMS messages (SIM)", - icon="mdi:email-receive", + icon="mdi:email-arrow-left", ), } diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 6886d500a84..9c2004f0edb 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -148,7 +148,7 @@ def icon_for_icloud_device(icloud_device: IcloudDevice) -> str: "iPad": "mdi:tablet", "iPhone": "mdi:cellphone", "iPod": "mdi:ipod", - "iMac": "mdi:desktop-mac", + "iMac": "mdi:monitor", "MacBookPro": "mdi:laptop", } diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index eaa32362baf..cf6cc827519 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -78,7 +78,7 @@ DEVICE_ICONS = { 1: "mdi:book-open-variant", # Amazon Kindle 2: "mdi:android", # Android Device 3: "mdi:cellphone", # Android Phone - 4: "mdi:tablet-android", # Android Tablet + 4: "mdi:tablet", # Android Tablet 5: "mdi:router-wireless", # Apple Airport Express 6: "mdi:disc-player", # Blu-ray Player 7: "mdi:router-network", # Bridge @@ -87,7 +87,7 @@ DEVICE_ICONS = { 10: "mdi:router-network", # Router 11: "mdi:play-network", # DVR 12: "mdi:gamepad-variant", # Gaming Console - 13: "mdi:desktop-mac", # iMac + 13: "mdi:monitor", # iMac 14: "mdi:tablet", # iPad 15: "mdi:tablet", # iPad Mini 16: "mdi:cellphone", # iPhone 5/5S/5C From 1d7d2875e154220547e5f9cb830f75e450ac2d7b Mon Sep 17 00:00:00 2001 From: Thibault Cohen <47721+titilambert@users.noreply.github.com> Date: Thu, 21 Jul 2022 06:36:49 -0400 Subject: [PATCH 609/820] Add websocket command recorder/import_statistics (#73937) * Expose ws_add_external_statistics in websocket API * Refactor * Add tests * Improve test coverage Co-authored-by: Thibault Cohen Co-authored-by: Erik --- homeassistant/components/recorder/core.py | 8 +- .../components/recorder/statistics.py | 67 ++++- homeassistant/components/recorder/tasks.py | 8 +- .../components/recorder/websocket_api.py | 51 +++- tests/components/recorder/test_statistics.py | 207 ++++++++++--- .../components/recorder/test_websocket_api.py | 275 +++++++++++++++++- 6 files changed, 547 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 49e870b658f..f3ae79f9909 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -74,7 +74,7 @@ from .tasks import ( CommitTask, DatabaseLockTask, EventTask, - ExternalStatisticsTask, + ImportStatisticsTask, KeepAliveTask, PerodicCleanupTask, PurgeTask, @@ -480,11 +480,11 @@ class Recorder(threading.Thread): ) @callback - def async_external_statistics( + def async_import_statistics( self, metadata: StatisticMetaData, stats: Iterable[StatisticData] ) -> None: - """Schedule external statistics.""" - self.queue_task(ExternalStatisticsTask(metadata, stats)) + """Schedule import of statistics.""" + self.queue_task(ImportStatisticsTask(metadata, stats)) @callback def _async_setup_periodic_tasks(self) -> None: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 26221aa199b..4ebd5e17902 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -29,7 +29,7 @@ from homeassistant.const import ( VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder @@ -1360,6 +1360,54 @@ def _statistics_exists( return result["id"] if result else None +@callback +def _async_import_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + statistics: Iterable[StatisticData], +) -> None: + """Validate timestamps and insert an import_statistics job in the recorder's queue.""" + for statistic in statistics: + start = statistic["start"] + if start.tzinfo is None or start.tzinfo.utcoffset(start) is None: + raise HomeAssistantError("Naive timestamp") + if start.minute != 0 or start.second != 0 or start.microsecond != 0: + raise HomeAssistantError("Invalid timestamp") + statistic["start"] = dt_util.as_utc(start) + + if "last_reset" in statistic and statistic["last_reset"] is not None: + last_reset = statistic["last_reset"] + if ( + last_reset.tzinfo is None + or last_reset.tzinfo.utcoffset(last_reset) is None + ): + raise HomeAssistantError("Naive timestamp") + statistic["last_reset"] = dt_util.as_utc(last_reset) + + # Insert job in recorder's queue + hass.data[DATA_INSTANCE].async_import_statistics(metadata, statistics) + + +@callback +def async_import_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + statistics: Iterable[StatisticData], +) -> None: + """Import hourly statistics from an internal source. + + This inserts an import_statistics job in the recorder's queue. + """ + if not valid_entity_id(metadata["statistic_id"]): + raise HomeAssistantError("Invalid statistic_id") + + # The source must not be empty and must be aligned with the statistic_id + if not metadata["source"] or metadata["source"] != DOMAIN: + raise HomeAssistantError("Invalid source") + + _async_import_statistics(hass, metadata, statistics) + + @callback def async_add_external_statistics( hass: HomeAssistant, @@ -1368,7 +1416,7 @@ def async_add_external_statistics( ) -> None: """Add hourly statistics from an external source. - This inserts an add_external_statistics job in the recorder's queue. + This inserts an import_statistics job in the recorder's queue. """ # The statistic_id has same limitations as an entity_id, but with a ':' as separator if not valid_statistic_id(metadata["statistic_id"]): @@ -1379,16 +1427,7 @@ def async_add_external_statistics( if not metadata["source"] or metadata["source"] != domain: raise HomeAssistantError("Invalid source") - for statistic in statistics: - start = statistic["start"] - if start.tzinfo is None or start.tzinfo.utcoffset(start) is None: - raise HomeAssistantError("Naive timestamp") - if start.minute != 0 or start.second != 0 or start.microsecond != 0: - raise HomeAssistantError("Invalid timestamp") - statistic["start"] = dt_util.as_utc(start) - - # Insert job in recorder's queue - hass.data[DATA_INSTANCE].async_external_statistics(metadata, statistics) + _async_import_statistics(hass, metadata, statistics) def _filter_unique_constraint_integrity_error( @@ -1432,12 +1471,12 @@ def _filter_unique_constraint_integrity_error( @retryable_database_job("statistics") -def add_external_statistics( +def import_statistics( instance: Recorder, metadata: StatisticMetaData, statistics: Iterable[StatisticData], ) -> bool: - """Process an add_external_statistics job.""" + """Process an import_statistics job.""" with session_scope( session=instance.get_session(), diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 5ec83a3cefc..6d1c9c360ab 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -124,18 +124,18 @@ class StatisticsTask(RecorderTask): @dataclass -class ExternalStatisticsTask(RecorderTask): - """An object to insert into the recorder queue to run an external statistics task.""" +class ImportStatisticsTask(RecorderTask): + """An object to insert into the recorder queue to run an import statistics task.""" metadata: StatisticMetaData statistics: Iterable[StatisticData] def run(self, instance: Recorder) -> None: """Run statistics task.""" - if statistics.add_external_statistics(instance, self.metadata, self.statistics): + if statistics.import_statistics(instance, self.metadata, self.statistics): return # Schedule a new statistics task if this one didn't finish - instance.queue_task(ExternalStatisticsTask(self.metadata, self.statistics)) + instance.queue_task(ImportStatisticsTask(self.metadata, self.statistics)) @dataclass diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 45e2cf5620b..4ba5f3c8a8b 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -7,11 +7,17 @@ from typing import TYPE_CHECKING import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback, valid_entity_id +from homeassistant.helpers import config_validation as cv from homeassistant.util import dt as dt_util from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG -from .statistics import list_statistic_ids, validate_statistics +from .statistics import ( + async_add_external_statistics, + async_import_statistics, + list_statistic_ids, + validate_statistics, +) from .util import async_migration_in_progress if TYPE_CHECKING: @@ -31,6 +37,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_backup_start) websocket_api.async_register_command(hass, ws_backup_end) websocket_api.async_register_command(hass, ws_adjust_sum_statistics) + websocket_api.async_register_command(hass, ws_import_statistics) @websocket_api.websocket_command( @@ -136,6 +143,46 @@ def ws_adjust_sum_statistics( connection.send_result(msg["id"]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/import_statistics", + vol.Required("metadata"): { + vol.Required("has_mean"): bool, + vol.Required("has_sum"): bool, + vol.Required("name"): vol.Any(str, None), + vol.Required("source"): str, + vol.Required("statistic_id"): str, + vol.Required("unit_of_measurement"): vol.Any(str, None), + }, + vol.Required("stats"): [ + { + vol.Required("start"): cv.datetime, + vol.Optional("mean"): vol.Any(float, int), + vol.Optional("min"): vol.Any(float, int), + vol.Optional("max"): vol.Any(float, int), + vol.Optional("last_reset"): vol.Any(cv.datetime, None), + vol.Optional("state"): vol.Any(float, int), + vol.Optional("sum"): vol.Any(float, int), + } + ], + } +) +@callback +def ws_import_statistics( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Adjust sum statistics.""" + metadata = msg["metadata"] + stats = msg["stats"] + + if valid_entity_id(metadata["statistic_id"]): + async_import_statistics(hass, metadata, stats) + else: + async_add_external_statistics(hass, metadata, stats) + connection.send_result(msg["id"]) + + @websocket_api.websocket_command( { vol.Required("type"): "recorder/info", diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 48639790d0d..30a2926844b 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -17,6 +17,7 @@ from homeassistant.components.recorder.db_schema import StatisticsShortTerm from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( async_add_external_statistics, + async_import_statistics, delete_statistics_duplicates, delete_statistics_meta_duplicates, get_last_short_term_statistics, @@ -437,26 +438,45 @@ def test_statistics_duplicated(hass_recorder, caplog): caplog.clear() -async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): - """Test inserting external statistics.""" +@pytest.mark.parametrize("last_reset_str", ("2022-01-01T00:00:00+02:00", None)) +@pytest.mark.parametrize( + "source, statistic_id, import_fn", + ( + ("test", "test:total_energy_import", async_add_external_statistics), + ("recorder", "sensor.total_energy_import", async_import_statistics), + ), +) +async def test_import_statistics( + hass, + hass_ws_client, + recorder_mock, + caplog, + source, + statistic_id, + import_fn, + last_reset_str, +): + """Test importing statistics and inserting external statistics.""" client = await hass_ws_client() assert "Compiling statistics for" not in caplog.text assert "Statistics already compiled" not in caplog.text zero = dt_util.utcnow() + last_reset = dt_util.parse_datetime(last_reset_str) if last_reset_str else None + last_reset_utc_str = dt_util.as_utc(last_reset).isoformat() if last_reset else None period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) external_statistics1 = { "start": period1, - "last_reset": None, + "last_reset": last_reset, "state": 0, "sum": 2, } external_statistics2 = { "start": period2, - "last_reset": None, + "last_reset": last_reset, "state": 1, "sum": 3, } @@ -465,37 +485,35 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "has_mean": False, "has_sum": True, "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import", + "source": source, + "statistic_id": statistic_id, "unit_of_measurement": "kWh", } - async_add_external_statistics( - hass, external_metadata, (external_statistics1, external_statistics2) - ) + import_fn(hass, external_metadata, (external_statistics1, external_statistics2)) await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(0.0), "sum": approx(2.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -506,37 +524,37 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): { "has_mean": False, "has_sum": True, - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "name": "Total imported energy", - "source": "test", + "source": source, "unit_of_measurement": "kWh", } ] - metadata = get_metadata(hass, statistic_ids=("test:total_energy_import",)) + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) assert metadata == { - "test:total_energy_import": ( + statistic_id: ( 1, { "has_mean": False, "has_sum": True, "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import", + "source": source, + "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, ) } - last_stats = get_last_statistics(hass, 1, "test:total_energy_import", True) + last_stats = get_last_statistics(hass, 1, statistic_id, True) assert last_stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -550,13 +568,13 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "state": 5, "sum": 6, } - async_add_external_statistics(hass, external_metadata, (external_statistics,)) + import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": None, @@ -567,13 +585,13 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "sum": approx(6.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -586,34 +604,34 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "max": 1, "mean": 2, "min": 3, - "last_reset": None, + "last_reset": last_reset, "state": 4, "sum": 5, } - async_add_external_statistics(hass, external_metadata, (external_statistics,)) + import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": approx(1.0), "mean": approx(2.0), "min": approx(3.0), - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(4.0), "sum": approx(5.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -624,7 +642,7 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): { "id": 1, "type": "recorder/adjust_sum_statistics", - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, } @@ -635,26 +653,26 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": approx(1.0), "mean": approx(2.0), "min": approx(3.0), - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(4.0), "sum": approx(5.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(1003.0), }, @@ -670,11 +688,12 @@ def test_external_statistics_errors(hass_recorder, caplog): assert "Statistics already compiled" not in caplog.text zero = dt_util.utcnow() + last_reset = zero.replace(minute=0, second=0, microsecond=0) - timedelta(days=1) period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) _external_statistics = { "start": period1, - "last_reset": None, + "last_reset": last_reset, "state": 0, "sum": 2, } @@ -711,7 +730,7 @@ def test_external_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} - # Attempt to insert statistics for an naive starting time + # Attempt to insert statistics for a naive starting time external_metadata = {**_external_metadata} external_statistics = { **_external_statistics, @@ -734,6 +753,106 @@ def test_external_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + # Attempt to insert statistics with a naive last_reset + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "last_reset": last_reset.replace(tzinfo=None), + } + with pytest.raises(HomeAssistantError): + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + + +def test_import_statistics_errors(hass_recorder, caplog): + """Test validation of imported statistics.""" + hass = hass_recorder() + wait_recording_done(hass) + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + last_reset = zero.replace(minute=0, second=0, microsecond=0) - timedelta(days=1) + period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + + _external_statistics = { + "start": period1, + "last_reset": last_reset, + "state": 0, + "sum": 2, + } + + _external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "recorder", + "statistic_id": "sensor.total_energy_import", + "unit_of_measurement": "kWh", + } + + # Attempt to insert statistics for an external source + external_metadata = { + **_external_metadata, + "statistic_id": "test:total_energy_import", + } + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + + # Attempt to insert statistics for the wrong domain + external_metadata = {**_external_metadata, "source": "sensor"} + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + + # Attempt to insert statistics for a naive starting time + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "start": period1.replace(tzinfo=None), + } + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + + # Attempt to insert statistics for an invalid starting time + external_metadata = {**_external_metadata} + external_statistics = {**_external_statistics, "start": period1.replace(minute=1)} + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + + # Attempt to insert statistics with a naive last_reset + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "last_reset": last_reset.replace(tzinfo=None), + } + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + @pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"]) @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 55bbb13898e..a7ac01cf1d1 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -9,7 +9,13 @@ from pytest import approx from homeassistant.components import recorder from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.statistics import async_add_external_statistics +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, + get_metadata, + list_statistic_ids, + statistics_during_period, +) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM @@ -547,3 +553,270 @@ async def test_get_statistics_metadata( "unit_of_measurement": unit, } ] + + +@pytest.mark.parametrize( + "source, statistic_id", + ( + ("test", "test:total_energy_import"), + ("recorder", "sensor.total_energy_import"), + ), +) +async def test_import_statistics( + hass, hass_ws_client, recorder_mock, caplog, source, statistic_id +): + """Test importing statistics.""" + client = await hass_ws_client() + + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) + + external_statistics1 = { + "start": period1.isoformat(), + "last_reset": None, + "state": 0, + "sum": 2, + } + external_statistics2 = { + "start": period2.isoformat(), + "last_reset": None, + "state": 1, + "sum": 3, + } + + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + } + + await client.send_json( + { + "id": 1, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics1, external_statistics2], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(0.0), + "sum": approx(2.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + statistic_ids = list_statistic_ids(hass) # TODO + assert statistic_ids == [ + { + "has_mean": False, + "has_sum": True, + "statistic_id": statistic_id, + "name": "Total imported energy", + "source": source, + "unit_of_measurement": "kWh", + } + ] + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) + assert metadata == { + statistic_id: ( + 1, + { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + }, + ) + } + last_stats = get_last_statistics(hass, 1, statistic_id, True) + assert last_stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + + # Update the previously inserted statistics + external_statistics = { + "start": period1.isoformat(), + "last_reset": None, + "state": 5, + "sum": 6, + } + + await client.send_json( + { + "id": 2, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(5.0), + "sum": approx(6.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + + # Update the previously inserted statistics + external_statistics = { + "start": period1.isoformat(), + "max": 1, + "mean": 2, + "min": 3, + "last_reset": None, + "state": 4, + "sum": 5, + } + + await client.send_json( + { + "id": 3, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), + "last_reset": None, + "state": approx(4.0), + "sum": approx(5.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + + await client.send_json( + { + "id": 4, + "type": "recorder/adjust_sum_statistics", + "statistic_id": statistic_id, + "start_time": period2.isoformat(), + "adjustment": 1000.0, + } + ) + response = await client.receive_json() + assert response["success"] + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), + "last_reset": None, + "state": approx(4.0), + "sum": approx(5.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(1003.0), + }, + ] + } From b1ed1543c8d17c2e1c936ae80a0671f7d35e4c99 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 21 Jul 2022 13:07:42 +0200 Subject: [PATCH 610/820] Improve http decorator typing (#75541) --- homeassistant/components/auth/login_flow.py | 2 +- homeassistant/components/http/ban.py | 18 ++++++---- .../components/http/data_validator.py | 33 ++++++++++++------- .../components/repairs/websocket_api.py | 4 +-- homeassistant/helpers/data_entry_flow.py | 2 +- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index b24da92afdd..6cc9d94c7a6 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -257,7 +257,7 @@ class LoginFlowResourceView(LoginFlowBaseView): @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA)) @log_invalid_auth - async def post(self, request, flow_id, data): + async def post(self, request, data, flow_id): """Handle progressing a login flow request.""" client_id = data.pop("client_id") diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index ee8324b2791..d2f5f9d8ba5 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -2,17 +2,18 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from contextlib import suppress from datetime import datetime from http import HTTPStatus from ipaddress import IPv4Address, IPv6Address, ip_address import logging from socket import gethostbyaddr, herror -from typing import Any, Final +from typing import Any, Final, TypeVar -from aiohttp.web import Application, Request, StreamResponse, middleware +from aiohttp.web import Application, Request, Response, StreamResponse, middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import persistent_notification @@ -24,6 +25,9 @@ from homeassistant.util import dt as dt_util, yaml from .view import HomeAssistantView +_HassViewT = TypeVar("_HassViewT", bound=HomeAssistantView) +_P = ParamSpec("_P") + _LOGGER: Final = logging.getLogger(__name__) KEY_BAN_MANAGER: Final = "ha_banned_ips_manager" @@ -82,13 +86,13 @@ async def ban_middleware( def log_invalid_auth( - func: Callable[..., Awaitable[StreamResponse]] -) -> Callable[..., Awaitable[StreamResponse]]: + func: Callable[Concatenate[_HassViewT, Request, _P], Awaitable[Response]] +) -> Callable[Concatenate[_HassViewT, Request, _P], Coroutine[Any, Any, Response]]: """Decorate function to handle invalid auth or failed login attempts.""" async def handle_req( - view: HomeAssistantView, request: Request, *args: Any, **kwargs: Any - ) -> StreamResponse: + view: _HassViewT, request: Request, *args: _P.args, **kwargs: _P.kwargs + ) -> Response: """Try to log failed login attempts if response status >= BAD_REQUEST.""" resp = await func(view, request, *args, **kwargs) if resp.status >= HTTPStatus.BAD_REQUEST: diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index cc661d43fd8..6647a6436c5 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -1,17 +1,21 @@ """Decorator for view methods to help with data validation.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from http import HTTPStatus import logging -from typing import Any +from typing import Any, TypeVar from aiohttp import web +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from .view import HomeAssistantView +_HassViewT = TypeVar("_HassViewT", bound=HomeAssistantView) +_P = ParamSpec("_P") + _LOGGER = logging.getLogger(__name__) @@ -33,33 +37,40 @@ class RequestDataValidator: self._allow_empty = allow_empty def __call__( - self, method: Callable[..., Awaitable[web.StreamResponse]] - ) -> Callable: + self, + method: Callable[ + Concatenate[_HassViewT, web.Request, dict[str, Any], _P], + Awaitable[web.Response], + ], + ) -> Callable[ + Concatenate[_HassViewT, web.Request, _P], + Coroutine[Any, Any, web.Response], + ]: """Decorate a function.""" @wraps(method) async def wrapper( - view: HomeAssistantView, request: web.Request, *args: Any, **kwargs: Any - ) -> web.StreamResponse: + view: _HassViewT, request: web.Request, *args: _P.args, **kwargs: _P.kwargs + ) -> web.Response: """Wrap a request handler with data validation.""" - data = None + raw_data = None try: - data = await request.json() + raw_data = await request.json() except ValueError: if not self._allow_empty or (await request.content.read()) != b"": _LOGGER.error("Invalid JSON received") return view.json_message("Invalid JSON.", HTTPStatus.BAD_REQUEST) - data = {} + raw_data = {} try: - kwargs["data"] = self._schema(data) + data: dict[str, Any] = self._schema(raw_data) except vol.Invalid as err: _LOGGER.error("Data does not match schema: %s", err) return view.json_message( f"Message format incorrect: {err}", HTTPStatus.BAD_REQUEST ) - result = await method(view, request, *args, **kwargs) + result = await method(view, request, data, *args, **kwargs) return result return wrapper diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index b6a71773273..2e9fcc5f8e4 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -113,7 +113,7 @@ class RepairsFlowIndexView(FlowManagerIndexView): result = self._prepare_result_json(result) - return self.json(result) # pylint: disable=arguments-differ + return self.json(result) class RepairsFlowResourceView(FlowManagerResourceView): @@ -136,4 +136,4 @@ class RepairsFlowResourceView(FlowManagerResourceView): raise Unauthorized(permission=POLICY_EDIT) # pylint: disable=no-value-for-parameter - return await super().post(request, flow_id) # type: ignore[no-any-return] + return await super().post(request, flow_id) diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 444876a7674..428a62f0c9d 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -102,7 +102,7 @@ class FlowManagerResourceView(_BaseFlowManagerView): @RequestDataValidator(vol.Schema(dict), allow_empty=True) async def post( - self, request: web.Request, flow_id: str, data: dict[str, Any] + self, request: web.Request, data: dict[str, Any], flow_id: str ) -> web.Response: """Handle a POST request.""" try: From 8523c66bb5dc6f7046286494a44a0c58e192094d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 21 Jul 2022 13:56:55 +0200 Subject: [PATCH 611/820] Deprecate U.S. Citizenship and Immigration Services (USCIS) integration (#75562) --- homeassistant/components/uscis/manifest.json | 1 + homeassistant/components/uscis/sensor.py | 13 +++++++++++++ homeassistant/components/uscis/strings.json | 8 ++++++++ homeassistant/components/uscis/translations/en.json | 8 ++++++++ 4 files changed, 30 insertions(+) create mode 100644 homeassistant/components/uscis/strings.json create mode 100644 homeassistant/components/uscis/translations/en.json diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index 0680848f70a..882dc588eba 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -3,6 +3,7 @@ "name": "U.S. Citizenship and Immigration Services (USCIS)", "documentation": "https://www.home-assistant.io/integrations/uscis", "requirements": ["uscisstatus==0.1.1"], + "dependencies": ["repairs"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["uscisstatus"] diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index 564c449e8b6..b4719243a8b 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -7,6 +7,7 @@ import logging import uscisstatus import voluptuous as vol +from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -34,6 +35,18 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the platform in Home Assistant and Case Information.""" + create_issue( + hass, + "uscis", + "pending_removal", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The USCIS sensor component is deprecated and will be removed in Home Assistant 2022.10" + ) uscis = UscisSensor(config["case_id"], config[CONF_NAME]) uscis.update() if uscis.valid_case_id: diff --git a/homeassistant/components/uscis/strings.json b/homeassistant/components/uscis/strings.json new file mode 100644 index 00000000000..b8dec86db18 --- /dev/null +++ b/homeassistant/components/uscis/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The USCIS integration is being removed", + "description": "The U.S. Citizenship and Immigration Services (USCIS) integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/uscis/translations/en.json b/homeassistant/components/uscis/translations/en.json new file mode 100644 index 00000000000..24e7e9ceea0 --- /dev/null +++ b/homeassistant/components/uscis/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The U.S. Citizenship and Immigration Services (USCIS) integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The USCIS integration is being removed" + } + } +} \ No newline at end of file From f3c4bf571bad13afe3463f61b0062f7d4378f190 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 09:44:53 -0500 Subject: [PATCH 612/820] Raise on bad update data instead of log in PassiveBluetoothDataUpdateCoordinator (#75536) --- .../components/bluetooth/passive_update_coordinator.py | 7 ++----- .../bluetooth/test_passive_update_coordinator.py | 9 +++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index df4233452df..77b3fc54aba 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -261,12 +261,9 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): if not isinstance(new_data, PassiveBluetoothDataUpdate): self.last_update_success = False # type: ignore[unreachable] - self.logger.error( - "The update_method for %s returned %s instead of a PassiveBluetoothDataUpdate", - self.name, - new_data, + raise ValueError( + f"The update_method for {self.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" ) - return if not self.last_update_success: self.last_update_success = True diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index ca4ef6e909c..a377a8f4a46 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -7,6 +7,7 @@ import time from unittest.mock import MagicMock, patch from home_assistant_bluetooth import BluetoothServiceInfo +import pytest from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.bluetooth.passive_update_coordinator import ( @@ -337,7 +338,7 @@ async def test_exception_from_update_method(hass, caplog): cancel_coordinator() -async def test_bad_data_from_update_method(hass, caplog): +async def test_bad_data_from_update_method(hass): """Test we handle bad data from the update method.""" run_count = 0 @@ -373,9 +374,9 @@ async def test_bad_data_from_update_method(hass, caplog): assert coordinator.available is True # We should go unavailable once we get bad data - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert "update_method" in caplog.text - assert "bad_data" in caplog.text + with pytest.raises(ValueError): + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is False # We should go available again once we get good data again From 05b463b28254434ed9eecd4a68a74364551f1c7e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 21 Jul 2022 18:38:59 +0200 Subject: [PATCH 613/820] Migrate AccuWeather to new entity naming style (#75127) * Use new entity naming style * Move AccuWeatherSensorDescription and SENSOR consts to sensor platform * Remove duplicate code * Suggested change * Format url --- .../components/accuweather/__init__.py | 26 +- homeassistant/components/accuweather/const.py | 279 --------------- homeassistant/components/accuweather/model.py | 14 - .../components/accuweather/sensor.py | 319 ++++++++++++++++-- .../components/accuweather/weather.py | 28 +- 5 files changed, 323 insertions(+), 343 deletions(-) delete mode 100644 homeassistant/components/accuweather/model.py diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index d36ea4e8466..9123648a38d 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -11,12 +11,14 @@ from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN +from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -26,6 +28,7 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AccuWeather as config entry.""" api_key: str = entry.data[CONF_API_KEY] + name: str = entry.data[CONF_NAME] assert entry.unique_id is not None location_key = entry.unique_id forecast: bool = entry.options.get(CONF_FORECAST, False) @@ -35,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websession = async_get_clientsession(hass) coordinator = AccuWeatherDataUpdateCoordinator( - hass, websession, api_key, location_key, forecast + hass, websession, api_key, location_key, forecast, name ) await coordinator.async_config_entry_first_refresh() @@ -73,12 +76,27 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): api_key: str, location_key: str, forecast: bool, + name: str, ) -> None: """Initialize.""" self.location_key = location_key self.forecast = forecast self.is_metric = hass.config.units.is_metric - self.accuweather = AccuWeather(api_key, session, location_key=self.location_key) + self.accuweather = AccuWeather(api_key, session, location_key=location_key) + self.device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, location_key)}, + manufacturer=MANUFACTURER, + name=name, + # You don't need to provide specific details for the URL, + # so passing in _ characters is fine if the location key + # is correct + configuration_url=( + "http://accuweather.com/en/" + f"_/_/{location_key}/" + f"weather-forecast/{location_key}/" + ), + ) # Enabling the forecast download increases the number of requests per data # update, we use 40 minutes for current condition only and 80 minutes for diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 408d4700422..c1b90de09e7 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -20,22 +19,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ) -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_CUBIC_METER, - LENGTH_FEET, - LENGTH_INCHES, - LENGTH_METERS, - LENGTH_MILLIMETERS, - PERCENTAGE, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TIME_HOURS, - UV_INDEX, -) - -from .model import AccuWeatherSensorDescription API_IMPERIAL: Final = "Imperial" API_METRIC: Final = "Metric" @@ -45,7 +28,6 @@ CONF_FORECAST: Final = "forecast" DOMAIN: Final = "accuweather" MANUFACTURER: Final = "AccuWeather, Inc." MAX_FORECAST_DAYS: Final = 4 -NAME: Final = "AccuWeather" CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37], @@ -63,264 +45,3 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_SUNNY: [1, 2, 5], ATTR_CONDITION_WINDY: [32], } - -FORECAST_SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( - AccuWeatherSensorDescription( - key="CloudCoverDay", - icon="mdi:weather-cloudy", - name="Cloud Cover Day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="CloudCoverNight", - icon="mdi:weather-cloudy", - name="Cloud Cover Night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="Grass", - icon="mdi:grass", - name="Grass Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="HoursOfSun", - icon="mdi:weather-partly-cloudy", - name="Hours Of Sun", - unit_metric=TIME_HOURS, - unit_imperial=TIME_HOURS, - ), - AccuWeatherSensorDescription( - key="Mold", - icon="mdi:blur", - name="Mold Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="Ozone", - icon="mdi:vector-triangle", - name="Ozone", - unit_metric=None, - unit_imperial=None, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="Ragweed", - icon="mdi:sprout", - name="Ragweed Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureMax", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureMin", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShadeMax", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Shade Max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShadeMin", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Shade Min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="ThunderstormProbabilityDay", - icon="mdi:weather-lightning", - name="Thunderstorm Probability Day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - ), - AccuWeatherSensorDescription( - key="ThunderstormProbabilityNight", - icon="mdi:weather-lightning", - name="Thunderstorm Probability Night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - ), - AccuWeatherSensorDescription( - key="Tree", - icon="mdi:tree-outline", - name="Tree Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="UVIndex", - icon="mdi:weather-sunny", - name="UV Index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, - ), - AccuWeatherSensorDescription( - key="WindGustDay", - icon="mdi:weather-windy", - name="Wind Gust Day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="WindGustNight", - icon="mdi:weather-windy", - name="Wind Gust Night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="WindDay", - icon="mdi:weather-windy", - name="Wind Day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - ), - AccuWeatherSensorDescription( - key="WindNight", - icon="mdi:weather-windy", - name="Wind Night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - ), -) - -SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( - AccuWeatherSensorDescription( - key="ApparentTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="Apparent Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="Ceiling", - icon="mdi:weather-fog", - name="Cloud Ceiling", - unit_metric=LENGTH_METERS, - unit_imperial=LENGTH_FEET, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="CloudCover", - icon="mdi:weather-cloudy", - name="Cloud Cover", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="DewPoint", - device_class=SensorDeviceClass.TEMPERATURE, - name="Dew Point", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShade", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Shade", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="Precipitation", - icon="mdi:weather-rainy", - name="Precipitation", - unit_metric=LENGTH_MILLIMETERS, - unit_imperial=LENGTH_INCHES, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="PressureTendency", - device_class="accuweather__pressure_tendency", - icon="mdi:gauge", - name="Pressure Tendency", - unit_metric=None, - unit_imperial=None, - ), - AccuWeatherSensorDescription( - key="UVIndex", - icon="mdi:weather-sunny", - name="UV Index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="WetBulbTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="Wet Bulb Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="WindChillTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="Wind Chill Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="Wind", - icon="mdi:weather-windy", - name="Wind", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="WindGust", - icon="mdi:weather-windy", - name="Wind Gust", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), -) diff --git a/homeassistant/components/accuweather/model.py b/homeassistant/components/accuweather/model.py deleted file mode 100644 index e74a6d46057..00000000000 --- a/homeassistant/components/accuweather/model.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Type definitions for AccuWeather integration.""" -from __future__ import annotations - -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class AccuWeatherSensorDescription(SensorEntityDescription): - """Class describing AccuWeather sensor entities.""" - - unit_metric: str | None = None - unit_imperial: str | None = None diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 220575541ad..72182d4d635 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -1,14 +1,31 @@ """Support for the AccuWeather service.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any, cast -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_CUBIC_METER, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_METERS, + LENGTH_MILLIMETERS, + PERCENTAGE, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TIME_HOURS, + UV_INDEX, +) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -20,28 +37,292 @@ from .const import ( ATTR_FORECAST, ATTRIBUTION, DOMAIN, - FORECAST_SENSOR_TYPES, - MANUFACTURER, MAX_FORECAST_DAYS, - NAME, - SENSOR_TYPES, ) -from .model import AccuWeatherSensorDescription PARALLEL_UPDATES = 1 +@dataclass +class AccuWeatherSensorDescription(SensorEntityDescription): + """Class describing AccuWeather sensor entities.""" + + unit_metric: str | None = None + unit_imperial: str | None = None + + +FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( + AccuWeatherSensorDescription( + key="CloudCoverDay", + icon="mdi:weather-cloudy", + name="Cloud cover day", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="CloudCoverNight", + icon="mdi:weather-cloudy", + name="Cloud cover night", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Grass", + icon="mdi:grass", + name="Grass pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="HoursOfSun", + icon="mdi:weather-partly-cloudy", + name="Hours of sun", + unit_metric=TIME_HOURS, + unit_imperial=TIME_HOURS, + ), + AccuWeatherSensorDescription( + key="Mold", + icon="mdi:blur", + name="Mold pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Ozone", + icon="mdi:vector-triangle", + name="Ozone", + unit_metric=None, + unit_imperial=None, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Ragweed", + icon="mdi:sprout", + name="Ragweed pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureMax", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature max", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureMin", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature min", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMax", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature shade max", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMin", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature shade min", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="ThunderstormProbabilityDay", + icon="mdi:weather-lightning", + name="Thunderstorm probability day", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + ), + AccuWeatherSensorDescription( + key="ThunderstormProbabilityNight", + icon="mdi:weather-lightning", + name="Thunderstorm probability night", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + ), + AccuWeatherSensorDescription( + key="Tree", + icon="mdi:tree-outline", + name="Tree pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + name="UV index", + unit_metric=UV_INDEX, + unit_imperial=UV_INDEX, + ), + AccuWeatherSensorDescription( + key="WindGustDay", + icon="mdi:weather-windy", + name="Wind gust day", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="WindGustNight", + icon="mdi:weather-windy", + name="Wind gust night", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="WindDay", + icon="mdi:weather-windy", + name="Wind day", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + ), + AccuWeatherSensorDescription( + key="WindNight", + icon="mdi:weather-windy", + name="Wind night", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + ), +) + +SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( + AccuWeatherSensorDescription( + key="ApparentTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Apparent temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Ceiling", + icon="mdi:weather-fog", + name="Cloud ceiling", + unit_metric=LENGTH_METERS, + unit_imperial=LENGTH_FEET, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="CloudCover", + icon="mdi:weather-cloudy", + name="Cloud cover", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="DewPoint", + device_class=SensorDeviceClass.TEMPERATURE, + name="Dew point", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShade", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature shade", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Precipitation", + icon="mdi:weather-rainy", + name="Precipitation", + unit_metric=LENGTH_MILLIMETERS, + unit_imperial=LENGTH_INCHES, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="PressureTendency", + device_class="accuweather__pressure_tendency", + icon="mdi:gauge", + name="Pressure tendency", + unit_metric=None, + unit_imperial=None, + ), + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + name="UV index", + unit_metric=UV_INDEX, + unit_imperial=UV_INDEX, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WetBulbTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Wet bulb temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WindChillTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Wind chill temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Wind", + icon="mdi:weather-windy", + name="Wind", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WindGust", + icon="mdi:weather-windy", + name="Wind gust", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add AccuWeather entities from a config_entry.""" - name: str = entry.data[CONF_NAME] coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors: list[AccuWeatherSensor] = [] for description in SENSOR_TYPES: - sensors.append(AccuWeatherSensor(name, coordinator, description)) + sensors.append(AccuWeatherSensor(coordinator, description)) if coordinator.forecast: for description in FORECAST_SENSOR_TYPES: @@ -50,9 +331,7 @@ async def async_setup_entry( # locations. if description.key in coordinator.data[ATTR_FORECAST][0]: sensors.append( - AccuWeatherSensor( - name, coordinator, description, forecast_day=day - ) + AccuWeatherSensor(coordinator, description, forecast_day=day) ) async_add_entities(sensors) @@ -64,11 +343,11 @@ class AccuWeatherSensor( """Define an AccuWeather entity.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True entity_description: AccuWeatherSensorDescription def __init__( self, - name: str, coordinator: AccuWeatherDataUpdateCoordinator, description: AccuWeatherSensorDescription, forecast_day: int | None = None, @@ -81,12 +360,11 @@ class AccuWeatherSensor( ) self._attrs: dict[str, Any] = {} if forecast_day is not None: - self._attr_name = f"{name} {description.name} {forecast_day}d" + self._attr_name = f"{description.name} {forecast_day}d" self._attr_unique_id = ( f"{coordinator.location_key}-{description.key}-{forecast_day}".lower() ) else: - self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( f"{coordinator.location_key}-{description.key}".lower() ) @@ -96,12 +374,7 @@ class AccuWeatherSensor( else: self._unit_system = API_IMPERIAL self._attr_native_unit_of_measurement = description.unit_imperial - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, coordinator.location_key)}, - manufacturer=MANUFACTURER, - name=NAME, - ) + self._attr_device_info = coordinator.device_info self.forecast_day = forecast_day @property diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index ae1824aef4a..e8a5b0ab396 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -18,7 +18,6 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_NAME, LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, @@ -31,8 +30,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utc_from_timestamp @@ -45,8 +42,6 @@ from .const import ( ATTRIBUTION, CONDITION_CLASSES, DOMAIN, - MANUFACTURER, - NAME, ) PARALLEL_UPDATES = 1 @@ -56,11 +51,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add a AccuWeather weather entity from a config_entry.""" - name: str = entry.data[CONF_NAME] coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([AccuWeatherEntity(name, coordinator)]) + async_add_entities([AccuWeatherEntity(coordinator)]) class AccuWeatherEntity( @@ -68,9 +62,9 @@ class AccuWeatherEntity( ): """Define an AccuWeather entity.""" - def __init__( - self, name: str, coordinator: AccuWeatherDataUpdateCoordinator - ) -> None: + _attr_has_entity_name = True + + def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None: """Initialize.""" super().__init__(coordinator) # Coordinator data is used also for sensors which don't have units automatically @@ -90,21 +84,9 @@ class AccuWeatherEntity( self._attr_native_temperature_unit = TEMP_FAHRENHEIT self._attr_native_visibility_unit = LENGTH_MILES self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR - self._attr_name = name self._attr_unique_id = coordinator.location_key self._attr_attribution = ATTRIBUTION - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, coordinator.location_key)}, - manufacturer=MANUFACTURER, - name=NAME, - # You don't need to provide specific details for the URL, - # so passing in _ characters is fine if the location key - # is correct - configuration_url="http://accuweather.com/en/" - f"_/_/{coordinator.location_key}/" - f"weather-forecast/{coordinator.location_key}/", - ) + self._attr_device_info = coordinator.device_info @property def condition(self) -> str | None: From 6cb1794720fa6df4ffe5c3fc431a31afbba6b5e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 15:58:45 -0500 Subject: [PATCH 614/820] Bump aiohomekit to 1.1.9 (#75591) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 10ca50db820..c784bad0d06 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.8"], + "requirements": ["aiohomekit==1.1.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 26d7c6bce14..c88df779c25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.8 +aiohomekit==1.1.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 48826ebdf1a..57787b0cb72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.8 +aiohomekit==1.1.9 # homeassistant.components.emulated_hue # homeassistant.components.http From 04c6b9c51963418ffebddc7753939700fbea7e42 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 21 Jul 2022 17:54:50 -0400 Subject: [PATCH 615/820] ZHA light entity cleanup (#75573) * use base class attributes * initial hue and saturation support * spec is 65536 not 65535 * fixes * enhanced current hue * fix comparison * clean up * fix channel test * oops * report enhanced current hue --- .../components/zha/core/channels/lighting.py | 71 ++++ homeassistant/components/zha/light.py | 344 ++++++++++-------- tests/components/zha/test_channels.py | 13 +- tests/components/zha/test_light.py | 23 +- 4 files changed, 285 insertions(+), 166 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 99e6101b0bd..36bb0beb17d 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -31,6 +31,9 @@ class ColorChannel(ZigbeeChannel): REPORT_CONFIG = ( AttrReportConfig(attr="current_x", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="current_y", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="current_hue", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="enhanced_current_hue", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="current_saturation", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="color_temperature", config=REPORT_CONFIG_DEFAULT), ) MAX_MIREDS: int = 500 @@ -52,6 +55,14 @@ class ColorChannel(ZigbeeChannel): return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP return self.CAPABILITIES_COLOR_XY + @property + def zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities: + """Return ZCL color capabilities of the light.""" + color_capabilities = self.cluster.get("color_capabilities") + if color_capabilities is None: + return lighting.Color.ColorCapabilities(self.CAPABILITIES_COLOR_XY) + return lighting.Color.ColorCapabilities(color_capabilities) + @property def color_mode(self) -> int | None: """Return cached value of the color_mode attribute.""" @@ -77,6 +88,21 @@ class ColorChannel(ZigbeeChannel): """Return cached value of the current_y attribute.""" return self.cluster.get("current_y") + @property + def current_hue(self) -> int | None: + """Return cached value of the current_hue attribute.""" + return self.cluster.get("current_hue") + + @property + def enhanced_current_hue(self) -> int | None: + """Return cached value of the enhanced_current_hue attribute.""" + return self.cluster.get("enhanced_current_hue") + + @property + def current_saturation(self) -> int | None: + """Return cached value of the current_saturation attribute.""" + return self.cluster.get("current_saturation") + @property def min_mireds(self) -> int: """Return the coldest color_temp that this channel supports.""" @@ -86,3 +112,48 @@ class ColorChannel(ZigbeeChannel): def max_mireds(self) -> int: """Return the warmest color_temp that this channel supports.""" return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) + + @property + def hs_supported(self) -> bool: + """Return True if the channel supports hue and saturation.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Hue_and_saturation + in self.zcl_color_capabilities + ) + + @property + def enhanced_hue_supported(self) -> bool: + """Return True if the channel supports enhanced hue and saturation.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Enhanced_hue + in self.zcl_color_capabilities + ) + + @property + def xy_supported(self) -> bool: + """Return True if the channel supports xy.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.XY_attributes + in self.zcl_color_capabilities + ) + + @property + def color_temp_supported(self) -> bool: + """Return True if the channel supports color temperature.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Color_temperature + in self.zcl_color_capabilities + ) + + @property + def color_loop_supported(self) -> bool: + """Return True if the channel supports color loop.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Color_loop + in self.zcl_color_capabilities + ) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index cc8dc475c43..8aab3d20a5c 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -15,15 +15,6 @@ from zigpy.zcl.foundation import Status from homeassistant.components import light from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_MODE, - ATTR_COLOR_TEMP, - ATTR_EFFECT, - ATTR_EFFECT_LIST, - ATTR_HS_COLOR, - ATTR_MAX_MIREDS, - ATTR_MIN_MIREDS, - ATTR_SUPPORTED_COLOR_MODES, ColorMode, brightness_supported, filter_supported_color_modes, @@ -43,7 +34,6 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval -import homeassistant.util.color as color_util from .core import discovery, helpers from .core.const import ( @@ -69,10 +59,6 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -CAPABILITIES_COLOR_LOOP = 0x4 -CAPABILITIES_COLOR_XY = 0x08 -CAPABILITIES_COLOR_TEMP = 0x10 - DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 @@ -88,7 +74,7 @@ GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" -COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.HS} +COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} SUPPORT_GROUP_LIGHT = ( light.LightEntityFeature.EFFECT | light.LightEntityFeature.FLASH @@ -123,24 +109,18 @@ class BaseLight(LogMixin, light.LightEntity): def __init__(self, *args, **kwargs): """Initialize the light.""" super().__init__(*args, **kwargs) - self._available: bool = False - self._brightness: int | None = None + self._attr_min_mireds: int | None = 153 + self._attr_max_mireds: int | None = 500 + self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes + self._attr_supported_features: int = 0 + self._attr_state: bool | None self._off_with_transition: bool = False self._off_brightness: int | None = None - self._hs_color: tuple[float, float] | None = None - self._color_temp: int | None = None - self._min_mireds: int | None = 153 - self._max_mireds: int | None = 500 - self._effect_list: list[str] | None = None - self._effect: str | None = None - self._supported_features: int = 0 - self._state: bool = False + self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._on_off_channel = None self._level_channel = None self._color_channel = None self._identify_channel = None - self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME - self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes @property def extra_state_attributes(self) -> dict[str, Any]: @@ -154,24 +134,9 @@ class BaseLight(LogMixin, light.LightEntity): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state is None: + if self._attr_state is None: return False - return self._state - - @property - def brightness(self): - """Return the brightness of this light.""" - return self._brightness - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._min_mireds - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._max_mireds + return self._attr_state @callback def set_level(self, value): @@ -182,34 +147,9 @@ class BaseLight(LogMixin, light.LightEntity): level """ value = max(0, min(254, value)) - self._brightness = value + self._attr_brightness = value self.async_write_ha_state() - @property - def hs_color(self): - """Return the hs color value [int, int].""" - return self._hs_color - - @property - def color_temp(self): - """Return the CT color value in mireds.""" - return self._color_temp - - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._effect_list - - @property - def effect(self): - """Return the current effect.""" - return self._effect - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) @@ -222,6 +162,7 @@ class BaseLight(LogMixin, light.LightEntity): effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) temperature = kwargs.get(light.ATTR_COLOR_TEMP) + xy_color = kwargs.get(light.ATTR_XY_COLOR) hs_color = kwargs.get(light.ATTR_HS_COLOR) # If the light is currently off but a turn_on call with a color/temperature is sent, @@ -235,19 +176,26 @@ class BaseLight(LogMixin, light.LightEntity): new_color_provided_while_off = ( not isinstance(self, LightGroup) and not self._FORCE_ON - and not self._state + and not self._attr_state and ( ( temperature is not None and ( - self._color_temp != temperature + self._attr_color_temp != temperature or self._attr_color_mode != ColorMode.COLOR_TEMP ) ) + or ( + xy_color is not None + and ( + self._attr_xy_color != xy_color + or self._attr_color_mode != ColorMode.XY + ) + ) or ( hs_color is not None and ( - self.hs_color != hs_color + self._attr_hs_color != hs_color or self._attr_color_mode != ColorMode.HS ) ) @@ -265,7 +213,7 @@ class BaseLight(LogMixin, light.LightEntity): if brightness is not None: level = min(254, brightness) else: - level = self._brightness or 254 + level = self._attr_brightness or 254 t_log = {} @@ -280,7 +228,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call - self._state = True + self._attr_state = True if ( (brightness is not None or transition) @@ -294,9 +242,9 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._state = bool(level) + self._attr_state = bool(level) if level: - self._brightness = level + self._attr_brightness = level if ( brightness is None @@ -310,7 +258,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._state = True + self._attr_state = True if temperature is not None: result = await self._color_channel.move_to_color_temp( @@ -324,11 +272,39 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return self._attr_color_mode = ColorMode.COLOR_TEMP - self._color_temp = temperature - self._hs_color = None + self._attr_color_temp = temperature + self._attr_xy_color = None + self._attr_hs_color = None if hs_color is not None: - xy_color = color_util.color_hs_to_xy(*hs_color) + if self._color_channel.enhanced_hue_supported: + result = await self._color_channel.enhanced_move_to_hue_and_saturation( + int(hs_color[0] * 65535 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["enhanced_move_to_hue_and_saturation"] = result + else: + result = await self._color_channel.move_to_hue_and_saturation( + int(hs_color[0] * 254 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_hue_and_saturation"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + self.debug("turned on: %s", t_log) + return + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = hs_color + self._attr_xy_color = None + self._attr_color_temp = None + xy_color = None # don't set xy_color if it is also present + + if xy_color is not None: result = await self._color_channel.move_to_color( int(xy_color[0] * 65535), int(xy_color[1] * 65535), @@ -340,9 +316,10 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._attr_color_mode = ColorMode.HS - self._hs_color = hs_color - self._color_temp = None + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = xy_color + self._attr_color_temp = None + self._attr_hs_color = None if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. @@ -351,9 +328,9 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._state = bool(level) + self._attr_state = bool(level) if level: - self._brightness = level + self._attr_brightness = level if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( @@ -366,9 +343,10 @@ class BaseLight(LogMixin, light.LightEntity): 0, # no hue ) t_log["color_loop_set"] = result - self._effect = light.EFFECT_COLORLOOP + self._attr_effect = light.EFFECT_COLORLOOP elif ( - self._effect == light.EFFECT_COLORLOOP and effect != light.EFFECT_COLORLOOP + self._attr_effect == light.EFFECT_COLORLOOP + and effect != light.EFFECT_COLORLOOP ): result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION, @@ -378,7 +356,7 @@ class BaseLight(LogMixin, light.LightEntity): 0x0, # update action only, action off, no dir, time, hue ) t_log["color_loop_set"] = result - self._effect = None + self._attr_effect = None if flash is not None: result = await self._identify_channel.trigger_effect( @@ -406,12 +384,12 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned off: %s", result) if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return - self._state = False + self._attr_state = False if supports_level: # store current brightness so that the next turn_on uses it. self._off_with_transition = transition is not None - self._off_brightness = self._brightness + self._off_brightness = self._attr_brightness self.async_write_ha_state() @@ -427,44 +405,59 @@ class Light(BaseLight, ZhaEntity): """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] - self._state = bool(self._on_off_channel.on_off) + self._attr_state = bool(self._on_off_channel.on_off) self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) self._identify_channel = self.zha_device.channels.identify_ch if self._color_channel: - self._min_mireds: int | None = self._color_channel.min_mireds - self._max_mireds: int | None = self._color_channel.max_mireds + self._attr_min_mireds: int = self._color_channel.min_mireds + self._attr_max_mireds: int = self._color_channel.max_mireds self._cancel_refresh_handle = None effect_list = [] self._attr_supported_color_modes = {ColorMode.ONOFF} if self._level_channel: self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) - self._supported_features |= light.LightEntityFeature.TRANSITION - self._brightness = self._level_channel.current_level + self._attr_supported_features |= light.LightEntityFeature.TRANSITION + self._attr_brightness = self._level_channel.current_level if self._color_channel: - color_capabilities = self._color_channel.color_capabilities - if color_capabilities & CAPABILITIES_COLOR_TEMP: + if self._color_channel.color_temp_supported: self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) - self._color_temp = self._color_channel.color_temperature + self._attr_color_temp = self._color_channel.color_temperature - if color_capabilities & CAPABILITIES_COLOR_XY: - self._attr_supported_color_modes.add(ColorMode.HS) + if ( + self._color_channel.xy_supported + and not self._color_channel.hs_supported + ): + self._attr_supported_color_modes.add(ColorMode.XY) curr_x = self._color_channel.current_x curr_y = self._color_channel.current_y if curr_x is not None and curr_y is not None: - self._hs_color = color_util.color_xy_to_hs( - float(curr_x / 65535), float(curr_y / 65535) + self._attr_xy_color = (curr_x / 65535, curr_y / 65535) + else: + self._attr_xy_color = (0, 0) + + if self._color_channel.hs_supported: + self._attr_supported_color_modes.add(ColorMode.HS) + if self._color_channel.enhanced_hue_supported: + curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 + else: + curr_hue = self._color_channel.current_hue * 254 / 360 + curr_saturation = self._color_channel.current_saturation + if curr_hue is not None and curr_saturation is not None: + self._attr_hs_color = ( + int(curr_hue), + int(curr_saturation * 2.54), ) else: - self._hs_color = (0, 0) + self._attr_hs_color = (0, 0) - if color_capabilities & CAPABILITIES_COLOR_LOOP: - self._supported_features |= light.LightEntityFeature.EFFECT + if self._color_channel.color_loop_supported: + self._attr_supported_features |= light.LightEntityFeature.EFFECT effect_list.append(light.EFFECT_COLORLOOP) if self._color_channel.color_loop_active == 1: - self._effect = light.EFFECT_COLORLOOP + self._attr_effect = light.EFFECT_COLORLOOP self._attr_supported_color_modes = filter_supported_color_modes( self._attr_supported_color_modes ) @@ -475,13 +468,13 @@ class Light(BaseLight, ZhaEntity): if self._color_channel.color_mode == Color.ColorMode.Color_temperature: self._attr_color_mode = ColorMode.COLOR_TEMP else: - self._attr_color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.XY if self._identify_channel: - self._supported_features |= light.LightEntityFeature.FLASH + self._attr_supported_features |= light.LightEntityFeature.FLASH if effect_list: - self._effect_list = effect_list + self._attr_effect_list = effect_list self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, @@ -493,7 +486,7 @@ class Light(BaseLight, ZhaEntity): @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" - self._state = bool(value) + self._attr_state = bool(value) if value: self._off_with_transition = False self._off_brightness = None @@ -529,9 +522,9 @@ class Light(BaseLight, ZhaEntity): @callback def async_restore_last_state(self, last_state): """Restore previous state.""" - self._state = last_state.state == STATE_ON + self._attr_state = last_state.state == STATE_ON if "brightness" in last_state.attributes: - self._brightness = last_state.attributes["brightness"] + self._attr_brightness = last_state.attributes["brightness"] if "off_with_transition" in last_state.attributes: self._off_with_transition = last_state.attributes["off_with_transition"] if "off_brightness" in last_state.attributes: @@ -539,15 +532,17 @@ class Light(BaseLight, ZhaEntity): if "color_mode" in last_state.attributes: self._attr_color_mode = ColorMode(last_state.attributes["color_mode"]) if "color_temp" in last_state.attributes: - self._color_temp = last_state.attributes["color_temp"] + self._attr_color_temp = last_state.attributes["color_temp"] + if "xy_color" in last_state.attributes: + self._attr_xy_color = last_state.attributes["xy_color"] if "hs_color" in last_state.attributes: - self._hs_color = last_state.attributes["hs_color"] + self._attr_hs_color = last_state.attributes["hs_color"] if "effect" in last_state.attributes: - self._effect = last_state.attributes["effect"] + self._attr_effect = last_state.attributes["effect"] async def async_get_state(self): """Attempt to retrieve the state from the light.""" - if not self.available: + if not self._attr_available: return self.debug("polling current state") if self._on_off_channel: @@ -555,21 +550,32 @@ class Light(BaseLight, ZhaEntity): "on_off", from_cache=False ) if state is not None: - self._state = state + self._attr_state = state if self._level_channel: level = await self._level_channel.get_attribute_value( "current_level", from_cache=False ) if level is not None: - self._brightness = level + self._attr_brightness = level if self._color_channel: attributes = [ "color_mode", - "color_temperature", "current_x", "current_y", - "color_loop_active", ] + if self._color_channel.enhanced_hue_supported: + attributes.append("enhanced_current_hue") + attributes.append("current_saturation") + if ( + self._color_channel.hs_supported + and not self._color_channel.enhanced_hue_supported + ): + attributes.append("current_hue") + attributes.append("current_saturation") + if self._color_channel.color_temp_supported: + attributes.append("color_temperature") + if self._color_channel.color_loop_supported: + attributes.append("color_loop_active") results = await self._color_channel.get_attributes( attributes, from_cache=False, only_cache=False @@ -580,24 +586,40 @@ class Light(BaseLight, ZhaEntity): self._attr_color_mode = ColorMode.COLOR_TEMP color_temp = results.get("color_temperature") if color_temp is not None and color_mode: - self._color_temp = color_temp - self._hs_color = None - else: + self._attr_color_temp = color_temp + self._attr_xy_color = None + self._attr_hs_color = None + elif color_mode == Color.ColorMode.Hue_and_saturation: self._attr_color_mode = ColorMode.HS + if self._color_channel.enhanced_hue_supported: + current_hue = results.get("enhanced_current_hue") + else: + current_hue = results.get("current_hue") + current_saturation = results.get("current_saturation") + if current_hue is not None and current_saturation is not None: + self._attr_hs_color = ( + int(current_hue * 360 / 65535) + if self._color_channel.enhanced_hue_supported + else int(current_hue * 360 / 254), + int(current_saturation / 254), + ) + self._attr_xy_color = None + self._attr_color_temp = None + else: + self._attr_color_mode = Color.ColorMode.X_and_Y color_x = results.get("current_x") color_y = results.get("current_y") if color_x is not None and color_y is not None: - self._hs_color = color_util.color_xy_to_hs( - float(color_x / 65535), float(color_y / 65535) - ) - self._color_temp = None + self._attr_xy_color = (color_x / 65535, color_y / 65535) + self._attr_color_temp = None + self._attr_hs_color = None color_loop_active = results.get("color_loop_active") if color_loop_active is not None: if color_loop_active == 1: - self._effect = light.EFFECT_COLORLOOP + self._attr_effect = light.EFFECT_COLORLOOP else: - self._effect = None + self._attr_effect = None async def async_update(self): """Update to the latest state.""" @@ -671,6 +693,12 @@ class LightGroup(BaseLight, ZhaGroupEntity): ) self._attr_color_mode = None + # remove this when all ZHA platforms and base entities are updated + @property + def available(self) -> bool: + """Return entity availability.""" + return self._attr_available + async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() @@ -700,39 +728,49 @@ class LightGroup(BaseLight, ZhaGroupEntity): states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] - self._state = len(on_states) > 0 - self._available = any(state.state != STATE_UNAVAILABLE for state in states) + self._attr_state = len(on_states) > 0 + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) - self._brightness = helpers.reduce_attribute(on_states, ATTR_BRIGHTNESS) - - self._hs_color = helpers.reduce_attribute( - on_states, ATTR_HS_COLOR, reduce=helpers.mean_tuple + self._attr_brightness = helpers.reduce_attribute( + on_states, light.ATTR_BRIGHTNESS ) - self._color_temp = helpers.reduce_attribute(on_states, ATTR_COLOR_TEMP) - self._min_mireds = helpers.reduce_attribute( - states, ATTR_MIN_MIREDS, default=153, reduce=min - ) - self._max_mireds = helpers.reduce_attribute( - states, ATTR_MAX_MIREDS, default=500, reduce=max + self._attr_xy_color = helpers.reduce_attribute( + on_states, light.ATTR_XY_COLOR, reduce=helpers.mean_tuple ) - self._effect_list = None - all_effect_lists = list(helpers.find_state_attributes(states, ATTR_EFFECT_LIST)) + self._attr_hs_color = helpers.reduce_attribute( + on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple + ) + + self._attr_color_temp = helpers.reduce_attribute( + on_states, light.ATTR_COLOR_TEMP + ) + self._attr_min_mireds = helpers.reduce_attribute( + states, light.ATTR_MIN_MIREDS, default=153, reduce=min + ) + self._attr_max_mireds = helpers.reduce_attribute( + states, light.ATTR_MAX_MIREDS, default=500, reduce=max + ) + + self._attr_effect_list = None + all_effect_lists = list( + helpers.find_state_attributes(states, light.ATTR_EFFECT_LIST) + ) if all_effect_lists: # Merge all effects from all effect_lists with a union merge. - self._effect_list = list(set().union(*all_effect_lists)) + self._attr_effect_list = list(set().union(*all_effect_lists)) - self._effect = None - all_effects = list(helpers.find_state_attributes(on_states, ATTR_EFFECT)) + self._attr_effect = None + all_effects = list(helpers.find_state_attributes(on_states, light.ATTR_EFFECT)) if all_effects: # Report the most common effect. effects_count = Counter(itertools.chain(all_effects)) - self._effect = effects_count.most_common(1)[0][0] + self._attr_effect = effects_count.most_common(1)[0][0] self._attr_color_mode = None all_color_modes = list( - helpers.find_state_attributes(on_states, ATTR_COLOR_MODE) + helpers.find_state_attributes(on_states, light.ATTR_COLOR_MODE) ) if all_color_modes: # Report the most common color mode, select brightness and onoff last @@ -745,7 +783,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): self._attr_supported_color_modes = None all_supported_color_modes = list( - helpers.find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) + helpers.find_state_attributes(states, light.ATTR_SUPPORTED_COLOR_MODES) ) if all_supported_color_modes: # Merge all color modes. @@ -753,14 +791,14 @@ class LightGroup(BaseLight, ZhaGroupEntity): set[str], set().union(*all_supported_color_modes) ) - self._supported_features = 0 + self._attr_supported_features = 0 for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature # we find. - self._supported_features |= support + self._attr_supported_features |= support # Bitwise-and the supported features with the GroupedLight's features # so that we don't break in the future when a new feature is added. - self._supported_features &= SUPPORT_GROUP_LIGHT + self._attr_supported_features &= SUPPORT_GROUP_LIGHT async def _force_member_updates(self): """Force the update of member entities to ensure the states are correct for bulbs that don't report their state.""" diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 7701992cab4..6aba5500a2a 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -151,7 +151,18 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): }, ), (0x0202, 1, {"fan_mode"}), - (0x0300, 1, {"current_x", "current_y", "color_temperature"}), + ( + 0x0300, + 1, + { + "current_x", + "current_y", + "color_temperature", + "current_hue", + "enhanced_current_hue", + "current_saturation", + }, + ), (0x0400, 1, {"measured_value"}), (0x0401, 1, {"level_status"}), (0x0402, 1, {"measured_value"}), diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 982ff622341..f892448dc69 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -15,11 +15,7 @@ from homeassistant.components.light import ( ColorMode, ) from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.light import ( - CAPABILITIES_COLOR_TEMP, - CAPABILITIES_COLOR_XY, - FLASH_EFFECTS, -) +from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform import homeassistant.util.dt as dt_util @@ -148,7 +144,8 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -180,7 +177,8 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -239,7 +237,8 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -302,7 +301,7 @@ async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored ) @pytest.mark.parametrize( "device, reporting", - [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 3))], + [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 6))], ) async def test_light( hass, zigpy_device_mock, zha_device_joined_restored, device, reporting @@ -1400,7 +1399,7 @@ async def test_zha_group_light_entity( assert group_state.state == STATE_OFF assert group_state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, - ColorMode.HS, + ColorMode.XY, ] # Light which is off has no color mode assert "color_mode" not in group_state.attributes @@ -1431,9 +1430,9 @@ async def test_zha_group_light_entity( assert group_state.state == STATE_ON assert group_state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, - ColorMode.HS, + ColorMode.XY, ] - assert group_state.attributes["color_mode"] == ColorMode.HS + assert group_state.attributes["color_mode"] == ColorMode.XY # test long flashing the lights from the HA await async_test_flash_from_hass( From 975378ba44803f409c5ad4f0c805dbc83dadee5b Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 22 Jul 2022 01:46:16 +0200 Subject: [PATCH 616/820] Add ZHA config option for "enhanced light transition from an off-state" (#75151) * Add ZHA config option for "enhanced light transition from an off-state" * Default option to disabled * Always disable "enhanced light transition" for ZHA LightGroups * Rename _enhanced_light_transition to _zha_config_enhanced_light_transition * Remove LightGroup check, as config option always disables for groups * Remove duplicated line * Remove duplicated line * Move ZHA config transition line below other config line * Renamed comments of renamed variable in tests color_provided_while_off -> new_color_provided_while_off * Enable "enhanced light transitions" for testing --- homeassistant/components/zha/core/const.py | 2 ++ homeassistant/components/zha/light.py | 11 +++++++++- homeassistant/components/zha/strings.json | 1 + .../components/zha/translations/en.json | 1 + tests/components/zha/conftest.py | 5 ++++- tests/components/zha/test_light.py | 22 +++++++++---------- 6 files changed, 29 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index de373215e52..acac96f3162 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -128,6 +128,7 @@ CONF_CUSTOM_QUIRKS_PATH = "custom_quirks_path" CONF_DATABASE = "database_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" +CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_FLOWCONTROL = "flow_control" @@ -143,6 +144,7 @@ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, + vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Optional( CONF_CONSIDER_UNAVAILABLE_MAINS, diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 8aab3d20a5c..6c26e17188b 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -41,6 +41,7 @@ from .core.const import ( CHANNEL_LEVEL, CHANNEL_ON_OFF, CONF_DEFAULT_LIGHT_TRANSITION, + CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, DATA_ZHA, EFFECT_BLINK, EFFECT_BREATHE, @@ -117,6 +118,7 @@ class BaseLight(LogMixin, light.LightEntity): self._off_with_transition: bool = False self._off_brightness: int | None = None self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME + self._zha_config_enhanced_light_transition: bool = False self._on_off_channel = None self._level_channel = None self._color_channel = None @@ -174,7 +176,7 @@ class BaseLight(LogMixin, light.LightEntity): # move to level, on, color, move to level... We also will not set this if the bulb is already in the # desired color mode with the desired color or color temperature. new_color_provided_while_off = ( - not isinstance(self, LightGroup) + self._zha_config_enhanced_light_transition and not self._FORCE_ON and not self._attr_state and ( @@ -482,6 +484,12 @@ class Light(BaseLight, ZhaEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_enhanced_light_transition = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, + False, + ) @callback def async_set_state(self, attr_id, attr_name, value): @@ -691,6 +699,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_enhanced_light_transition = False self._attr_color_mode = None # remove this when all ZHA platforms and base entities are updated diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 5953df52e92..eb7753276f8 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -37,6 +37,7 @@ "config_panel": { "zha_options": { "title": "Global Options", + "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 00c78101a53..91c23acc044 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", + "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", "title": "Global Options" } }, diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index d9223027668..b1041a3e2a3 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -70,11 +70,14 @@ async def config_entry_fixture(hass): }, options={ zha_const.CUSTOM_CONFIGURATION: { + zha_const.ZHA_OPTIONS: { + zha_const.CONF_ENABLE_ENHANCED_LIGHT_TRANSITION: True, + }, zha_const.ZHA_ALARM_OPTIONS: { zha_const.CONF_ALARM_ARM_REQUIRES_CODE: False, zha_const.CONF_ALARM_MASTER_CODE: "4321", zha_const.CONF_ALARM_FAILED_TRIES: 2, - } + }, } }, ) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index f892448dc69..c5b02f1a02f 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -560,7 +560,7 @@ async def test_transitions( dev1_cluster_level.request.reset_mock() - # test non 0 length transition and color temp while turning light on (color_provided_while_off) + # test non 0 length transition and color temp while turning light on (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -596,7 +596,7 @@ async def test_transitions( 10, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, 235, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -645,7 +645,7 @@ async def test_transitions( dev1_cluster_color.request.reset_mock() dev1_cluster_level.request.reset_mock() - # test no transition provided and color temp while turning light on (color_provided_while_off) + # test no transition provided and color temp while turning light on (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -680,7 +680,7 @@ async def test_transitions( 10, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, 236, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -761,7 +761,7 @@ async def test_transitions( 10, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, 236, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -854,7 +854,7 @@ async def test_transitions( dev2_cluster_on_off.request.reset_mock() - # test non 0 length transition and color temp while turning light on and sengled (color_provided_while_off) + # test non 0 length transition and color temp while turning light on and sengled (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -890,7 +890,7 @@ async def test_transitions( 10, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, 235, # color temp mireds - 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -937,7 +937,7 @@ async def test_transitions( dev2_cluster_on_off.request.reset_mock() - # test non 0 length transition and color temp while turning group light on (color_provided_while_off) + # test non 0 length transition and color temp while turning group light on (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -960,13 +960,13 @@ async def test_transitions( assert group_level_channel.request.call_count == 1 assert group_level_channel.request.await_count == 1 - # groups are omitted from the 3 call dance for color_provided_while_off + # groups are omitted from the 3 call dance for new_color_provided_while_off assert group_color_channel.request.call_args == call( False, 10, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, 235, # color temp mireds - 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -1074,7 +1074,7 @@ async def test_transitions( dev2_cluster_level.request.reset_mock() - # test eWeLink color temp while turning light on from off (color_provided_while_off) + # test eWeLink color temp while turning light on from off (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", From 90ca3fe350a293d77f161d48164c5f25934a4f8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 19:16:45 -0500 Subject: [PATCH 617/820] Improve availability tracking and coordinator setup in bluetooth (#75582) --- .../components/bluetooth/__init__.py | 94 +++++-- homeassistant/components/bluetooth/models.py | 10 +- .../bluetooth/passive_update_coordinator.py | 100 +++---- .../components/bluetooth/strings.json | 16 ++ .../components/bluetooth/translations/en.json | 16 ++ tests/components/bluetooth/test_init.py | 53 ++++ .../test_passive_update_coordinator.py | 263 ++++++++++-------- 7 files changed, 357 insertions(+), 195 deletions(-) create mode 100644 homeassistant/components/bluetooth/strings.json create mode 100644 homeassistant/components/bluetooth/translations/en.json diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 5edc0053203..46d1e5e8332 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime, timedelta from enum import Enum import fnmatch import logging @@ -22,6 +23,7 @@ from homeassistant.core import ( callback as hass_callback, ) from homeassistant.helpers import discovery_flow +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.loader import ( @@ -39,6 +41,8 @@ _LOGGER = logging.getLogger(__name__) MAX_REMEMBER_ADDRESSES: Final = 2048 +UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 + SOURCE_LOCAL: Final = "local" @@ -160,6 +164,20 @@ def async_register_callback( return manager.async_register_callback(callback, match_dict) +@hass_callback +def async_track_unavailable( + hass: HomeAssistant, + callback: Callable[[str], None], + address: str, +) -> Callable[[], None]: + """Register to receive a callback when an address is unavailable. + + Returns a callback that can be used to cancel the registration. + """ + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_track_unavailable(callback, address) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matchers = await async_get_bluetooth(hass) @@ -231,6 +249,8 @@ class BluetoothManager: self._integration_matchers = integration_matchers self.scanner: HaBleakScanner | None = None self._cancel_device_detected: CALLBACK_TYPE | None = None + self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None + self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] @@ -251,6 +271,7 @@ class BluetoothManager: ) return install_multiple_bleak_catcher(self.scanner) + self.async_setup_unavailable_tracking() # We have to start it right away as some integrations might # need it straight away. _LOGGER.debug("Starting bluetooth scanner") @@ -261,6 +282,34 @@ class BluetoothManager: self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) await self.scanner.start() + @hass_callback + def async_setup_unavailable_tracking(self) -> None: + """Set up the unavailable tracking.""" + + @hass_callback + def _async_check_unavailable(now: datetime) -> None: + """Watch for unavailable devices.""" + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HA_BLEAK_SCANNER + history = set(scanner.history) + active = {device.address for device in scanner.discovered_devices} + disappeared = history.difference(active) + for address in disappeared: + del scanner.history[address] + if not (callbacks := self._unavailable_callbacks.get(address)): + continue + for callback in callbacks: + try: + callback(address) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in unavailable callback") + + self._cancel_unavailable_tracking = async_track_time_interval( + self.hass, + _async_check_unavailable, + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS), + ) + @hass_callback def _device_detected( self, device: BLEDevice, advertisement_data: AdvertisementData @@ -283,12 +332,13 @@ class BluetoothManager: } if matched_domains: self._matched[match_key] = True - _LOGGER.debug( - "Device detected: %s with advertisement_data: %s matched domains: %s", - device, - advertisement_data, - matched_domains, - ) + + _LOGGER.debug( + "Device detected: %s with advertisement_data: %s matched domains: %s", + device, + advertisement_data, + matched_domains, + ) if not matched_domains and not self._callbacks: return @@ -321,6 +371,21 @@ class BluetoothManager: service_info, ) + @hass_callback + def async_track_unavailable( + self, callback: Callable[[str], None], address: str + ) -> Callable[[], None]: + """Register a callback.""" + self._unavailable_callbacks.setdefault(address, []).append(callback) + + @hass_callback + def _async_remove_callback() -> None: + self._unavailable_callbacks[address].remove(callback) + if not self._unavailable_callbacks[address]: + del self._unavailable_callbacks[address] + + return _async_remove_callback + @hass_callback def async_register_callback( self, @@ -369,25 +434,17 @@ class BluetoothManager: def async_address_present(self, address: str) -> bool: """Return if the address is present.""" return bool( - models.HA_BLEAK_SCANNER - and any( - device.address == address - for device in models.HA_BLEAK_SCANNER.discovered_devices - ) + models.HA_BLEAK_SCANNER and address in models.HA_BLEAK_SCANNER.history ) @hass_callback def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: """Return if the address is present.""" if models.HA_BLEAK_SCANNER: - discovered = models.HA_BLEAK_SCANNER.discovered_devices history = models.HA_BLEAK_SCANNER.history return [ - BluetoothServiceInfoBleak.from_advertisement( - *history[device.address], SOURCE_LOCAL - ) - for device in discovered - if device.address in history + BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) + for device_adv in history.values() ] return [] @@ -396,6 +453,9 @@ class BluetoothManager: if self._cancel_device_detected: self._cancel_device_detected() self._cancel_device_detected = None + if self._cancel_unavailable_tracking: + self._cancel_unavailable_tracking() + self._cancel_unavailable_tracking = None if self.scanner: await self.scanner.stop() models.HA_BLEAK_SCANNER = None diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index fd7559aeefa..ffb0ad107ec 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Mapping import contextlib import logging from typing import Any, Final, cast @@ -14,7 +13,6 @@ from bleak.backends.scanner import ( AdvertisementDataCallback, BaseBleakScanner, ) -from lru import LRU # pylint: disable=no-name-in-module from homeassistant.core import CALLBACK_TYPE, callback as hass_callback @@ -24,8 +22,6 @@ FILTER_UUIDS: Final = "UUIDs" HA_BLEAK_SCANNER: HaBleakScanner | None = None -MAX_HISTORY_SIZE: Final = 512 - def _dispatch_callback( callback: AdvertisementDataCallback, @@ -57,9 +53,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] self._callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] - self.history: Mapping[str, tuple[BLEDevice, AdvertisementData]] = LRU( - MAX_HISTORY_SIZE - ) + self.history: dict[str, tuple[BLEDevice, AdvertisementData]] = {} super().__init__(*args, **kwargs) @hass_callback @@ -90,7 +84,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] Here we get the actual callback from bleak and dispatch it to all the wrapped HaBleakScannerWrapper classes """ - self.history[device.address] = (device, advertisement_data) # type: ignore[index] + self.history[device.address] = (device, advertisement_data) for callback_filters in self._callbacks: _dispatch_callback(*callback_filters, device, advertisement_data) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 77b3fc54aba..b2fdd5d7d58 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,9 +1,8 @@ """The Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping import dataclasses -from datetime import datetime import logging import time from typing import Any, Generic, TypeVar @@ -14,19 +13,15 @@ from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later from . import ( BluetoothCallbackMatcher, BluetoothChange, - async_address_present, async_register_callback, + async_track_unavailable, ) from .const import DOMAIN -UNAVAILABLE_SECONDS = 60 * 5 -NEVER_TIME = -UNAVAILABLE_SECONDS - @dataclasses.dataclass(frozen=True) class PassiveBluetoothEntityKey: @@ -49,10 +44,13 @@ class PassiveBluetoothDataUpdate(Generic[_T]): """Generic bluetooth data.""" devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) - entity_descriptions: dict[ + entity_descriptions: Mapping[ PassiveBluetoothEntityKey, EntityDescription ] = dataclasses.field(default_factory=dict) - entity_data: dict[PassiveBluetoothEntityKey, _T] = dataclasses.field( + entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field( + default_factory=dict + ) + entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field( default_factory=dict ) @@ -106,6 +104,7 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): ] = {} self.update_method = update_method + self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} self.entity_descriptions: dict[ PassiveBluetoothEntityKey, EntityDescription @@ -113,54 +112,45 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self.devices: dict[str | None, DeviceInfo] = {} self.last_update_success = True - self._last_callback_time: float = NEVER_TIME - self._cancel_track_available: CALLBACK_TYPE | None = None - self._present = False + self._cancel_track_unavailable: CALLBACK_TYPE | None = None + self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None + self.present = False + self.last_seen = 0.0 @property def available(self) -> bool: """Return if the device is available.""" - return self._present and self.last_update_success + return self.present and self.last_update_success @callback - def _async_cancel_available_tracker(self) -> None: - """Reset the available tracker.""" - if self._cancel_track_available: - self._cancel_track_available() - self._cancel_track_available = None - - @callback - def _async_schedule_available_tracker(self, time_remaining: float) -> None: - """Schedule the available tracker.""" - self._cancel_track_available = async_call_later( - self.hass, time_remaining, self._async_check_device_present - ) - - @callback - def _async_check_device_present(self, _: datetime) -> None: - """Check if the device is present.""" - time_passed_since_seen = time.monotonic() - self._last_callback_time - self._async_cancel_available_tracker() - if ( - not self._present - or time_passed_since_seen < UNAVAILABLE_SECONDS - or async_address_present(self.hass, self.address) - ): - self._async_schedule_available_tracker( - UNAVAILABLE_SECONDS - time_passed_since_seen - ) - return - self._present = False + def _async_handle_unavailable(self, _address: str) -> None: + """Handle the device going unavailable.""" + self.present = False self.async_update_listeners(None) @callback - def async_setup(self) -> CALLBACK_TYPE: - """Start the callback.""" - return async_register_callback( + def _async_start(self) -> None: + """Start the callbacks.""" + self._cancel_bluetooth_advertisements = async_register_callback( self.hass, self._async_handle_bluetooth_event, BluetoothCallbackMatcher(address=self.address), ) + self._cancel_track_unavailable = async_track_unavailable( + self.hass, + self._async_handle_unavailable, + self.address, + ) + + @callback + def _async_stop(self) -> None: + """Stop the callbacks.""" + if self._cancel_bluetooth_advertisements is not None: + self._cancel_bluetooth_advertisements() + self._cancel_bluetooth_advertisements = None + if self._cancel_track_unavailable is not None: + self._cancel_track_unavailable() + self._cancel_track_unavailable = None @callback def async_add_entities_listener( @@ -199,10 +189,22 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): def remove_listener() -> None: """Remove update listener.""" self._listeners.remove(update_callback) + self._async_handle_listeners_changed() self._listeners.append(update_callback) + self._async_handle_listeners_changed() return remove_listener + @callback + def _async_handle_listeners_changed(self) -> None: + """Handle listeners changed.""" + has_listeners = self._listeners or self._entity_key_listeners + running = bool(self._cancel_bluetooth_advertisements) + if running and not has_listeners: + self._async_stop() + elif not running and has_listeners: + self._async_start() + @callback def async_add_entity_key_listener( self, @@ -217,8 +219,10 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self._entity_key_listeners[entity_key].remove(update_callback) if not self._entity_key_listeners[entity_key]: del self._entity_key_listeners[entity_key] + self._async_handle_listeners_changed() self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) + self._async_handle_listeners_changed() return remove_listener @callback @@ -242,11 +246,9 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" + self.last_seen = time.monotonic() self.name = service_info.name - self._last_callback_time = time.monotonic() - self._present = True - if not self._cancel_track_available: - self._async_schedule_available_tracker(UNAVAILABLE_SECONDS) + self.present = True if self.hass.is_stopping: return @@ -272,6 +274,7 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self.devices.update(new_data.devices) self.entity_descriptions.update(new_data.entity_descriptions) self.entity_data.update(new_data.entity_data) + self.entity_names.update(new_data.entity_names) self.async_update_listeners(new_data) @@ -315,6 +318,7 @@ class PassiveBluetoothCoordinatorEntity( self._attr_unique_id = f"{address}-{key}" if ATTR_NAME not in self._attr_device_info: self._attr_device_info[ATTR_NAME] = self.coordinator.name + self._attr_name = coordinator.entity_names.get(entity_key) @property def available(self) -> bool: diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json new file mode 100644 index 00000000000..925e9c512cc --- /dev/null +++ b/homeassistant/components/bluetooth/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Choose a device to setup", + "data": { + "address": "Device" + } + }, + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + } + } + } +} diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json new file mode 100644 index 00000000000..f75dc2603db --- /dev/null +++ b/homeassistant/components/bluetooth/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 1a002e5e354..e76ad559305 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" +from datetime import timedelta from unittest.mock import MagicMock, patch from bleak import BleakError @@ -7,12 +8,18 @@ from bleak.backends.scanner import AdvertisementData, BLEDevice from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( SOURCE_LOCAL, + UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, + async_track_unavailable, models, ) from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed async def test_setup_and_stop(hass, mock_bleak_scanner_start): @@ -241,9 +248,55 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False + + @callback + def _wrong_device_unavailable_callback(_address: str) -> None: + """Wrong device unavailable callback.""" + nonlocal wrong_device_went_unavailable + wrong_device_went_unavailable = True + raise ValueError("blow up") + + @callback + def _switchbot_device_unavailable_callback(_address: str) -> None: + """Switchbot device unavailable callback.""" + nonlocal switchbot_device_went_unavailable + switchbot_device_went_unavailable = True + + wrong_device_unavailable_cancel = async_track_unavailable( + hass, _wrong_device_unavailable_callback, wrong_device.address + ) + switchbot_device_unavailable_cancel = async_track_unavailable( + hass, _switchbot_device_unavailable_callback, switchbot_device.address + ) + + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) await hass.async_block_till_done() service_infos = bluetooth.async_discovered_service_info(hass) + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is True + + # See the devices again + models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + # Cancel the callbacks + wrong_device_unavailable_cancel() + switchbot_device_unavailable_cancel() + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False + + # Verify the cancel is effective + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is False + assert len(service_infos) == 1 # wrong_name should not appear because bleak no longer sees it assert service_infos[0].name == "wohand" diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index a377a8f4a46..755b2ec07f8 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -3,15 +3,17 @@ from __future__ import annotations from datetime import timedelta import logging -import time from unittest.mock import MagicMock, patch from home_assistant_bluetooth import BluetoothServiceInfo import pytest -from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.bluetooth import ( + DOMAIN, + UNAVAILABLE_TRACK_SECONDS, + BluetoothChange, +) from homeassistant.components.bluetooth.passive_update_coordinator import ( - UNAVAILABLE_SECONDS, PassiveBluetoothCoordinatorEntity, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, @@ -21,6 +23,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescr from homeassistant.const import TEMP_CELSIUS from homeassistant.core import CoreState, callback from homeassistant.helpers.entity import DeviceInfo +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import MockEntityPlatform, async_fire_time_changed @@ -49,16 +52,18 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( PassiveBluetoothEntityKey("temperature", None): 14.5, PassiveBluetoothEntityKey("pressure", None): 1234, }, + entity_names={ + PassiveBluetoothEntityKey("temperature", None): "Temperature", + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, entity_descriptions={ PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( key="temperature", - name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( key="pressure", - name="Pressure", native_unit_of_measurement="hPa", device_class=SensorDeviceClass.PRESSURE, ), @@ -66,8 +71,9 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_basic_usage(hass): +async def test_basic_usage(hass, mock_bleak_scanner_start): """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback def _async_generate_mock_data( @@ -91,35 +97,36 @@ async def test_basic_usage(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() - entity_key = PassiveBluetoothEntityKey("temperature", None) - entity_key_events = [] - all_events = [] - mock_entity = MagicMock() - mock_add_entities = MagicMock() + entity_key = PassiveBluetoothEntityKey("temperature", None) + entity_key_events = [] + all_events = [] + mock_entity = MagicMock() + mock_add_entities = MagicMock() - def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock entity key listener.""" - entity_key_events.append(data) + def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock entity key listener.""" + entity_key_events.append(data) - cancel_async_add_entity_key_listener = coordinator.async_add_entity_key_listener( - _async_entity_key_listener, - entity_key, - ) + cancel_async_add_entity_key_listener = ( + coordinator.async_add_entity_key_listener( + _async_entity_key_listener, + entity_key, + ) + ) - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) - cancel_listener = coordinator.async_add_listener( - _all_listener, - ) + cancel_listener = coordinator.async_add_listener( + _all_listener, + ) - cancel_async_add_entities_listener = coordinator.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + cancel_async_add_entities_listener = coordinator.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) @@ -155,11 +162,15 @@ async def test_basic_usage(hass): assert len(mock_entity.mock_calls) == 2 assert coordinator.available is True - cancel_coordinator() - -async def test_unavailable_after_no_data(hass): +async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): """Test that the coordinator is unavailable after no data for a while.""" + with patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() @callback def _async_generate_mock_data( @@ -183,14 +194,13 @@ async def test_unavailable_after_no_data(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() - mock_entity = MagicMock() - mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + mock_entity = MagicMock() + mock_add_entities = MagicMock() + coordinator.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) assert coordinator.available is False @@ -198,52 +208,40 @@ async def test_unavailable_after_no_data(hass): assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - monotonic_now = time.monotonic() - now = dt_util.utcnow() with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", - return_value=monotonic_now + UNAVAILABLE_SECONDS, + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch( + "homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): - async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) await hass.async_block_till_done() assert coordinator.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - # Now simulate the device is still present even though we got - # no data for a while - - monotonic_now = time.monotonic() - now = dt_util.utcnow() with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_address_present", - return_value=True, + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], ), patch( - "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", - return_value=monotonic_now + UNAVAILABLE_SECONDS, + "homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): - async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) - await hass.async_block_till_done() - - assert coordinator.available is True - - # And finally that it can go unavailable again when its gone - monotonic_now = time.monotonic() - now = dt_util.utcnow() - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", - return_value=monotonic_now + UNAVAILABLE_SECONDS, - ): - async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) await hass.async_block_till_done() assert coordinator.available is False - cancel_coordinator() - -async def test_no_updates_once_stopping(hass): +async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): """Test updates are ignored once hass is stopping.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback def _async_generate_mock_data( @@ -267,17 +265,16 @@ async def test_no_updates_once_stopping(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() - all_events = [] + all_events = [] - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) - coordinator.async_add_listener( - _all_listener, - ) + coordinator.async_add_listener( + _all_listener, + ) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 @@ -288,11 +285,11 @@ async def test_no_updates_once_stopping(hass): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 - cancel_coordinator() - -async def test_exception_from_update_method(hass, caplog): +async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): """Test we handle exceptions from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + run_count = 0 @callback @@ -321,7 +318,7 @@ async def test_exception_from_update_method(hass, caplog): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() + coordinator.async_add_listener(MagicMock()) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True @@ -335,11 +332,11 @@ async def test_exception_from_update_method(hass, caplog): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True - cancel_coordinator() - -async def test_bad_data_from_update_method(hass): +async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): """Test we handle bad data from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + run_count = 0 @callback @@ -368,7 +365,7 @@ async def test_bad_data_from_update_method(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() + coordinator.async_add_listener(MagicMock()) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True @@ -383,8 +380,6 @@ async def test_bad_data_from_update_method(hass): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True - cancel_coordinator() - GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( name="B5178D6FB", @@ -429,7 +424,6 @@ GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( force_update=False, icon=None, has_entity_name=False, - name="Temperature", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="°C", @@ -446,7 +440,6 @@ GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( force_update=False, icon=None, has_entity_name=False, - name="Humidity", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -463,7 +456,6 @@ GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( force_update=False, icon=None, has_entity_name=False, - name="Battery", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -480,13 +472,20 @@ GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( force_update=False, icon=None, has_entity_name=False, - name="Signal Strength", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="dBm", state_class=None, ), }, + entity_names={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + }, entity_data={ PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, @@ -520,7 +519,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Temperature", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="°C", @@ -537,7 +535,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Humidity", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -554,7 +551,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Battery", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -571,7 +567,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Signal Strength", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="dBm", @@ -588,7 +583,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Temperature", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="°C", @@ -605,7 +599,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Humidity", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -622,7 +615,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Battery", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -639,13 +631,30 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Signal Strength", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="dBm", state_class=None, ), }, + entity_names={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="primary"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): "Signal Strength", + }, entity_data={ PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, @@ -660,8 +669,9 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( ) -async def test_integration_with_entity(hass): +async def test_integration_with_entity(hass, mock_bleak_scanner_start): """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) update_count = 0 @@ -691,7 +701,7 @@ async def test_integration_with_entity(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_setup() + coordinator.async_add_listener(MagicMock()) mock_add_entities = MagicMock() @@ -770,8 +780,9 @@ NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_integration_with_entity_without_a_device(hass): +async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start): """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback def _async_generate_mock_data( @@ -795,14 +806,13 @@ async def test_integration_with_entity_without_a_device(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_setup() - mock_add_entities = MagicMock() + mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, - mock_add_entities, - ) + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + mock_add_entities, + ) saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) # First call with just the remote sensor entities results in them being added @@ -826,8 +836,12 @@ async def test_integration_with_entity_without_a_device(hass): ) -async def test_passive_bluetooth_entity_with_entity_platform(hass): +async def test_passive_bluetooth_entity_with_entity_platform( + hass, mock_bleak_scanner_start +): """Test with a mock entity platform.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + entity_platform = MockEntityPlatform(hass) @callback @@ -852,18 +866,23 @@ async def test_passive_bluetooth_entity_with_entity_platform(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_setup() - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, - lambda entities: hass.async_create_task( - entity_platform.async_add_entities(entities) - ), + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + lambda entities: hass.async_create_task( + entity_platform.async_add_entities(entities) + ), + ) + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") + is not None + ) + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") + is not None ) - - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - await hass.async_block_till_done() - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - await hass.async_block_till_done() - assert hass.states.get("test_domain.temperature") is not None - assert hass.states.get("test_domain.pressure") is not None From 36138afb93100eabd4059fe7c003fb5730e1c25f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 22 Jul 2022 00:29:27 +0000 Subject: [PATCH 618/820] [ci skip] Translation update --- .../components/demo/translations/de.json | 21 +++++++++++++++++++ .../components/demo/translations/et.json | 21 +++++++++++++++++++ .../components/demo/translations/pl.json | 21 +++++++++++++++++++ .../components/demo/translations/tr.json | 21 +++++++++++++++++++ .../components/google/translations/tr.json | 3 ++- .../homekit_controller/translations/tr.json | 4 ++-- .../components/lifx/translations/pl.json | 20 ++++++++++++++++++ .../components/lifx/translations/tr.json | 20 ++++++++++++++++++ .../components/mqtt/translations/bg.json | 2 ++ .../components/plugwise/translations/de.json | 3 ++- .../components/plugwise/translations/pl.json | 3 ++- .../components/plugwise/translations/tr.json | 3 ++- .../components/powerwall/translations/bg.json | 1 + .../components/uscis/translations/ca.json | 7 +++++++ .../components/uscis/translations/et.json | 8 +++++++ .../components/uscis/translations/fr.json | 7 +++++++ .../components/uscis/translations/pl.json | 8 +++++++ .../components/uscis/translations/pt-BR.json | 8 +++++++ .../uscis/translations/zh-Hant.json | 8 +++++++ .../components/zha/translations/pt-BR.json | 1 + 20 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/uscis/translations/ca.json create mode 100644 homeassistant/components/uscis/translations/et.json create mode 100644 homeassistant/components/uscis/translations/fr.json create mode 100644 homeassistant/components/uscis/translations/pl.json create mode 100644 homeassistant/components/uscis/translations/pt-BR.json create mode 100644 homeassistant/components/uscis/translations/zh-Hant.json diff --git a/homeassistant/components/demo/translations/de.json b/homeassistant/components/demo/translations/de.json index fd6239fa787..ab06043d52c 100644 --- a/homeassistant/components/demo/translations/de.json +++ b/homeassistant/components/demo/translations/de.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Dr\u00fccke OK, wenn die Blinkerfl\u00fcssigkeit nachgef\u00fcllt wurde.", + "title": "Blinkerfl\u00fcssigkeit muss nachgef\u00fcllt werden" + } + } + }, + "title": "Die Blinkerfl\u00fcssigkeit ist leer und muss nachgef\u00fcllt werden" + }, + "transmogrifier_deprecated": { + "description": "Die Transmogrifier-Komponente ist jetzt veraltet, da die neue API keine lokale Kontrolle mehr bietet.", + "title": "Die Transmogrifier-Komponente ist veraltet" + }, + "unfixable_problem": { + "description": "Dieses Problem wird niemals aufgeben.", + "title": "Dieses Problem kann nicht behoben werden" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/et.json b/homeassistant/components/demo/translations/et.json index ca06ed9c3bd..0e4c89dba01 100644 --- a/homeassistant/components/demo/translations/et.json +++ b/homeassistant/components/demo/translations/et.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Vajuta OK kui Blinkeri vedelik on uuesti t\u00e4idetud", + "title": "Blinkeri vedelikku on vaja uuesti t\u00e4ita" + } + } + }, + "title": "Blinkeri vedelik on otsas ja seda tuleb uuesti t\u00e4ita" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier komponent on n\u00fc\u00fcd aegunud, kuna uues API-s puudub kohalik kontroll", + "title": "Transmogrifieri komponent on aegunud" + }, + "unfixable_problem": { + "description": "See teema ei anna kunagi alla.", + "title": "See ei ole lahendatav probleem" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/pl.json b/homeassistant/components/demo/translations/pl.json index bc9e6701c65..c57a1e4f619 100644 --- a/homeassistant/components/demo/translations/pl.json +++ b/homeassistant/components/demo/translations/pl.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Naci\u015bnij OK po uzupe\u0142nieniu p\u0142ynu \u015bwiate\u0142ka", + "title": "P\u0142yn \u015bwiate\u0142ka nale\u017cy uzupe\u0142ni\u0107" + } + } + }, + "title": "P\u0142yn \u015bwiate\u0142ka jest pusty i nale\u017cy go uzupe\u0142ni\u0107" + }, + "transmogrifier_deprecated": { + "description": "Komponent transmogryfikatora jest ju\u017c przestarza\u0142y z powodu braku lokalnej kontroli w nowym API", + "title": "Komponent transmogryfikatora jest przestarza\u0142y" + }, + "unfixable_problem": { + "description": "Ten problem nigdy si\u0119 nie podda.", + "title": "Problem jest nie do naprawienia" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json index 1eea23e1bc5..b7ff98f9cd4 100644 --- a/homeassistant/components/demo/translations/tr.json +++ b/homeassistant/components/demo/translations/tr.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Fla\u015f\u00f6r yeniden dolduruldu\u011funda Tamam'a bas\u0131n", + "title": "Fla\u015f\u00f6r\u00fcn yeniden doldurulmas\u0131 gerekiyor" + } + } + }, + "title": "Fla\u015f\u00f6r bo\u015f ve yeniden doldurulmas\u0131 gerekiyor" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier bile\u015feni, yeni API'de mevcut olan yerel kontrol eksikli\u011fi nedeniyle art\u0131k kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015ft\u0131r", + "title": "Transmogrifier bile\u015feni kullan\u0131mdan kald\u0131r\u0131ld\u0131" + }, + "unfixable_problem": { + "description": "Bu konudan asla vazge\u00e7ilmeyecek.", + "title": "Bu d\u00fczeltilebilir bir sorun de\u011fil" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/tr.json b/homeassistant/components/google/translations/tr.json index ec265926695..7d67018630f 100644 --- a/homeassistant/components/google/translations/tr.json +++ b/homeassistant/components/google/translations/tr.json @@ -11,7 +11,8 @@ "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131" }, "create_entry": { "default": "Ba\u015far\u0131yla do\u011fruland\u0131" diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json index 7ddb32ade8e..3086e3e34fb 100644 --- a/homeassistant/components/homekit_controller/translations/tr.json +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -18,7 +18,7 @@ "unable_to_pair": "E\u015fle\u015ftirilemiyor, l\u00fctfen tekrar deneyin.", "unknown_error": "Cihaz bilinmeyen bir hata bildirdi. E\u015fle\u015ftirme ba\u015far\u0131s\u0131z oldu." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "T\u00fcm denetleyicilerde e\u015fle\u015ftirmeyi durdurun veya cihaz\u0131 yeniden ba\u015flatmay\u0131 deneyin, ard\u0131ndan e\u015fle\u015ftirmeye devam edin.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "G\u00fcvenli olmayan kurulum kodlar\u0131yla e\u015fle\u015ftirmeye izin verin.", "pairing_code": "E\u015fle\u015ftirme Kodu" }, - "description": "HomeKit Denetleyici, ayr\u0131 bir HomeKit denetleyicisi veya iCloud olmadan g\u00fcvenli bir \u015fifreli ba\u011flant\u0131 kullanarak yerel alan a\u011f\u0131 \u00fczerinden {name} ile ileti\u015fim kurar. Bu aksesuar\u0131 kullanmak i\u00e7in HomeKit e\u015fle\u015ftirme kodunuzu (XXX-XX-XXX bi\u00e7iminde) girin. Bu kod genellikle cihaz\u0131n kendisinde veya ambalaj\u0131nda bulunur.", + "description": "HomeKit Denetleyici, ayr\u0131 bir HomeKit denetleyicisi veya iCloud olmadan g\u00fcvenli bir \u015fifreli ba\u011flant\u0131 kullanarak yerel alan a\u011f\u0131 \u00fczerinden {name} ( {category} ) ile ileti\u015fim kurar. Bu aksesuar\u0131 kullanmak i\u00e7in HomeKit e\u015fle\u015ftirme kodunuzu (XXX-XX-XXX bi\u00e7iminde) girin. Bu kod genellikle cihaz\u0131n kendisinde veya ambalaj\u0131nda bulunur.", "title": "HomeKit Aksesuar Protokol\u00fc arac\u0131l\u0131\u011f\u0131yla bir cihazla e\u015fle\u015ftirin" }, "protocol_error": { diff --git a/homeassistant/components/lifx/translations/pl.json b/homeassistant/components/lifx/translations/pl.json index a8ee3fa57ac..817867d7c62 100644 --- a/homeassistant/components/lifx/translations/pl.json +++ b/homeassistant/components/lifx/translations/pl.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Urz\u0105dzenie" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Je\u015bli nie podasz IP lub nazwy hosta, zostanie u\u017cyte wykrywanie do odnalezienia urz\u0105dze\u0144." } } } diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json index ca4cfa92020..0f212e225be 100644 --- a/homeassistant/components/lifx/translations/tr.json +++ b/homeassistant/components/lifx/translations/tr.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "LIFX'i kurmak istiyor musunuz?" + }, + "discovery_confirm": { + "description": "{label} ( {host} ) {serial} kurmak istiyor musunuz?" + }, + "pick_device": { + "data": { + "device": "Cihaz" + } + }, + "user": { + "data": { + "host": "Sunucu" + }, + "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } } } diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index 65260eacabb..93b1d77dcfa 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -29,6 +29,8 @@ }, "device_automation": { "trigger_subtype": { + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" } }, diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 2b9d112977a..fb80fecef25 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert", + "anna_with_adam": "Sowohl Anna als auch Adam entdeckt. F\u00fcge deinen Adam anstelle deiner Anna hinzu" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 3d6a3a3b354..8de8da8e4e9 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "anna_with_adam": "Wykryto zar\u00f3wno Ann\u0119, jak i Adama. Dodaj Adama zamiast Anny." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index def3c6f8436..41f52761dbf 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "anna_with_adam": "Anna ve Adam tespit edildi. Anna'n\u0131z\u0131n yerine Adam'\u0131n\u0131z\u0131 ekleyin" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/powerwall/translations/bg.json b/homeassistant/components/powerwall/translations/bg.json index 12186a54eec..f0092b14bc1 100644 --- a/homeassistant/components/powerwall/translations/bg.json +++ b/homeassistant/components/powerwall/translations/bg.json @@ -3,6 +3,7 @@ "abort": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({ip_address})?" diff --git a/homeassistant/components/uscis/translations/ca.json b/homeassistant/components/uscis/translations/ca.json new file mode 100644 index 00000000000..7c836867d89 --- /dev/null +++ b/homeassistant/components/uscis/translations/ca.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "S'est\u00e0 eliminant la integraci\u00f3 USCIS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/et.json b/homeassistant/components/uscis/translations/et.json new file mode 100644 index 00000000000..0dc9325b715 --- /dev/null +++ b/homeassistant/components/uscis/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "USA kodakondsus- ja immigratsiooniteenistuse (USCIS) integratsioon ootab eemaldamist Home Assistantist ja ei ole enam k\u00e4ttesaadav alates Home Assistant 2022.10.\n\nIntegratsioon eemaldatakse, sest see p\u00f5hineb veebiotsingul, mis ei ole lubatud.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "USCIS-i sidumine eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/fr.json b/homeassistant/components/uscis/translations/fr.json new file mode 100644 index 00000000000..8d9e1c3f8ba --- /dev/null +++ b/homeassistant/components/uscis/translations/fr.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "L'int\u00e9gration USCIS est en cours de suppression" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/pl.json b/homeassistant/components/uscis/translations/pl.json new file mode 100644 index 00000000000..82e02996f7c --- /dev/null +++ b/homeassistant/components/uscis/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja US Citizenship and Immigration Services (USCIS) oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c opiera si\u0119 na webscrapingu, co jest niedozwolone. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Trwa usuwanie integracji USCIS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/pt-BR.json b/homeassistant/components/uscis/translations/pt-BR.json new file mode 100644 index 00000000000..76182bfa2d2 --- /dev/null +++ b/homeassistant/components/uscis/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o dos Servi\u00e7os de Cidadania e Imigra\u00e7\u00e3o dos EUA (USCIS) est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, pois depende de webscraping, o que n\u00e3o \u00e9 permitido. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do USCIS est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/zh-Hant.json b/homeassistant/components/uscis/translations/zh-Hant.json new file mode 100644 index 00000000000..4a5882dbd95 --- /dev/null +++ b/homeassistant/components/uscis/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u6b63\u8a08\u5283\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u8acb\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u6574\u5408\u6b63\u5728\u79fb\u9664\u4e2d\u3001\u7531\u65bc\u4f7f\u7528\u4e86\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u5c07\u4e0d\u88ab\u5141\u8a31\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "USCIS \u6574\u5408\u6b63\u6e96\u5099\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 8c43bf7f3e6..ba54b4aba87 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Considerar os dispositivos alimentados pela rede indispon\u00edveis ap\u00f3s (segundos)", "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz padr\u00e3o (segundos)", "enable_identify_on_join": "Ativar o efeito de identifica\u00e7\u00e3o quando os dispositivos ingressarem na rede", + "enhanced_light_transition": "Ative a transi\u00e7\u00e3o de cor/temperatura da luz aprimorada de um estado desligado", "title": "Op\u00e7\u00f5es globais" } }, From 67e16d77e8627b88b3dd811f9b3328cac2873d58 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 20:31:23 -0500 Subject: [PATCH 619/820] Add SensorPush BLE integration (#75531) --- CODEOWNERS | 2 + .../components/sensorpush/__init__.py | 56 +++++++ .../components/sensorpush/config_flow.py | 93 ++++++++++++ homeassistant/components/sensorpush/const.py | 3 + .../components/sensorpush/manifest.json | 15 ++ homeassistant/components/sensorpush/sensor.py | 143 ++++++++++++++++++ .../components/sensorpush/strings.json | 21 +++ .../sensorpush/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/sensorpush/__init__.py | 34 +++++ tests/components/sensorpush/conftest.py | 8 + .../components/sensorpush/test_config_flow.py | 98 ++++++++++++ tests/components/sensorpush/test_sensor.py | 50 ++++++ 16 files changed, 555 insertions(+) create mode 100644 homeassistant/components/sensorpush/__init__.py create mode 100644 homeassistant/components/sensorpush/config_flow.py create mode 100644 homeassistant/components/sensorpush/const.py create mode 100644 homeassistant/components/sensorpush/manifest.json create mode 100644 homeassistant/components/sensorpush/sensor.py create mode 100644 homeassistant/components/sensorpush/strings.json create mode 100644 homeassistant/components/sensorpush/translations/en.json create mode 100644 tests/components/sensorpush/__init__.py create mode 100644 tests/components/sensorpush/conftest.py create mode 100644 tests/components/sensorpush/test_config_flow.py create mode 100644 tests/components/sensorpush/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index dbbb21f0d6c..2a3999779fc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -922,6 +922,8 @@ build.json @home-assistant/supervisor /tests/components/sensibo/ @andrey-git @gjohansson-ST /homeassistant/components/sensor/ @home-assistant/core /tests/components/sensor/ @home-assistant/core +/homeassistant/components/sensorpush/ @bdraco +/tests/components/sensorpush/ @bdraco /homeassistant/components/sentry/ @dcramer @frenck /tests/components/sentry/ @dcramer @frenck /homeassistant/components/senz/ @milanmeu diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py new file mode 100644 index 00000000000..89e2287be70 --- /dev/null +++ b/homeassistant/components/sensorpush/__init__.py @@ -0,0 +1,56 @@ +"""The SensorPush Bluetooth integration.""" +from __future__ import annotations + +import logging + +from sensorpush_ble import SensorPushBluetoothDeviceData + +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from .const import DOMAIN +from .sensor import sensor_update_to_bluetooth_data_update + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up SensorPush BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + + data = SensorPushBluetoothDeviceData() + + @callback + def _async_update_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Update data from SensorPush Bluetooth.""" + return sensor_update_to_bluetooth_data_update(data.update(service_info)) + + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothDataUpdateCoordinator( + hass, + _LOGGER, + update_method=_async_update_data, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py new file mode 100644 index 00000000000..1a8e8b47abe --- /dev/null +++ b/homeassistant/components/sensorpush/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for sensorpush integration.""" +from __future__ import annotations + +from typing import Any + +from sensorpush_ble import SensorPushBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for sensorpush.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/sensorpush/const.py b/homeassistant/components/sensorpush/const.py new file mode 100644 index 00000000000..8f566c72d07 --- /dev/null +++ b/homeassistant/components/sensorpush/const.py @@ -0,0 +1,3 @@ +"""Constants for the SensorPush Bluetooth integration.""" + +DOMAIN = "sensorpush" diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json new file mode 100644 index 00000000000..08212f88132 --- /dev/null +++ b/homeassistant/components/sensorpush/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "sensorpush", + "name": "SensorPush", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sensorpush", + "bluetooth": [ + { + "local_name": "SensorPush*" + } + ], + "requirements": ["sensorpush-ble==1.4.2"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py new file mode 100644 index 00000000000..147555990e4 --- /dev/null +++ b/homeassistant/components/sensorpush/sensor.py @@ -0,0 +1,143 @@ +"""Support for sensorpush ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, + PassiveBluetoothEntityKey, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SensorPush BLE sensors.""" + coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + entry.async_on_unload( + coordinator.async_add_entities_listener( + SensorPushBluetoothSensorEntity, async_add_entities + ) + ) + + +class SensorPushBluetoothSensorEntity( + PassiveBluetoothCoordinatorEntity[ + PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a sensorpush ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.coordinator.entity_data.get(self.entity_key) diff --git a/homeassistant/components/sensorpush/strings.json b/homeassistant/components/sensorpush/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/sensorpush/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/sensorpush/translations/en.json b/homeassistant/components/sensorpush/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 03486b6043c..e3a4edc11d4 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -14,6 +14,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ 6 ] }, + { + "domain": "sensorpush", + "local_name": "SensorPush*" + }, { "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3c02842e856..95050ea6dbd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -308,6 +308,7 @@ FLOWS = { "sense", "senseme", "sensibo", + "sensorpush", "sentry", "senz", "sharkiq", diff --git a/requirements_all.txt b/requirements_all.txt index c88df779c25..f125a49c0a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2154,6 +2154,9 @@ sendgrid==6.8.2 # homeassistant.components.sense sense_energy==0.10.4 +# homeassistant.components.sensorpush +sensorpush-ble==1.4.2 + # homeassistant.components.sentry sentry-sdk==1.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57787b0cb72..605eb8352ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1438,6 +1438,9 @@ securetar==2022.2.0 # homeassistant.components.sense sense_energy==0.10.4 +# homeassistant.components.sensorpush +sensorpush-ble==1.4.2 + # homeassistant.components.sentry sentry-sdk==1.7.2 diff --git a/tests/components/sensorpush/__init__.py b/tests/components/sensorpush/__init__.py new file mode 100644 index 00000000000..0fe9ced64df --- /dev/null +++ b/tests/components/sensorpush/__init__.py @@ -0,0 +1,34 @@ +"""Tests for the SensorPush integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +HTW_SERVICE_INFO = BluetoothServiceInfo( + name="SensorPush HT.w 0CA1", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={11271: b"\xfe\x00\x01"}, + service_data={}, + service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090ab0"], + source="local", +) + +HTPWX_SERVICE_INFO = BluetoothServiceInfo( + name="SensorPush HTP.xw F4D", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={7168: b"\xcd=!\xd1\xb9"}, + service_data={}, + service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090ab0"], + source="local", +) diff --git a/tests/components/sensorpush/conftest.py b/tests/components/sensorpush/conftest.py new file mode 100644 index 00000000000..c6497a4e76d --- /dev/null +++ b/tests/components/sensorpush/conftest.py @@ -0,0 +1,8 @@ +"""SensorPush session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def auto_mock_bleak_scanner_start(mock_bleak_scanner_start): + """Auto mock bleak scanner start.""" diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py new file mode 100644 index 00000000000..662841af3f1 --- /dev/null +++ b/tests/components/sensorpush/test_config_flow.py @@ -0,0 +1,98 @@ +"""Test the SensorPush config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.sensorpush.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import HTPWX_SERVICE_INFO, HTW_SERVICE_INFO, NOT_SENSOR_PUSH_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTPWX_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "HTP.xw F4D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_bluetooth_not_sensorpush(hass): + """Test discovery via bluetooth not sensorpush.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_SENSOR_PUSH_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "HT.w 0CA1" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py new file mode 100644 index 00000000000..c48b8bc3407 --- /dev/null +++ b/tests/components/sensorpush/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the SensorPush config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.sensorpush.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import HTPWX_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(HTPWX_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.htp_xw_f4d_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "20.11" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "HTP.xw F4D Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From b0261dd2eb8b03a6ecf431a282f1be5120887e72 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 21 Jul 2022 20:32:42 -0600 Subject: [PATCH 620/820] Modify Guardian to store a single dataclass in `hass.data` (#75454) * Modify Guardian to store a single dataclass in `hass.data` * Clarity is better * Allow entry unload to cancel task --- homeassistant/components/guardian/__init__.py | 162 ++++++++---------- .../components/guardian/binary_sensor.py | 15 +- homeassistant/components/guardian/button.py | 19 +- homeassistant/components/guardian/const.py | 4 - .../components/guardian/diagnostics.py | 15 +- homeassistant/components/guardian/sensor.py | 15 +- homeassistant/components/guardian/switch.py | 19 +- tests/components/guardian/test_diagnostics.py | 12 +- 8 files changed, 112 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index d15b7d57aea..da22066d7aa 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -35,9 +35,6 @@ from .const import ( API_VALVE_STATUS, API_WIFI_STATUS, CONF_UID, - DATA_CLIENT, - DATA_COORDINATOR, - DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN, LOGGER, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -89,6 +86,16 @@ SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +@dataclass +class GuardianData: + """Define an object to be stored in `hass.data`.""" + + entry: ConfigEntry + client: Client + valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] + paired_sensor_manager: PairedSensorManager + + @callback def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: """Get the entry ID related to a service call (by device ID).""" @@ -131,7 +138,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_lock = asyncio.Lock() # Set up GuardianDataUpdateCoordinators for the valve controller: - coordinators: dict[str, GuardianDataUpdateCoordinator] = {} + valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] for api, api_coro in ( (API_SENSOR_PAIR_DUMP, client.sensor.pair_dump), @@ -140,7 +147,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (API_VALVE_STATUS, client.valve.status), (API_WIFI_STATUS, client.wifi.status), ): - coordinator = coordinators[api] = GuardianDataUpdateCoordinator( + coordinator = valve_controller_coordinators[ + api + ] = GuardianDataUpdateCoordinator( hass, client=client, api_name=api, @@ -154,45 +163,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up an object to evaluate each batch of paired sensor UIDs and add/remove # devices as appropriate: - paired_sensor_manager = PairedSensorManager(hass, entry, client, api_lock) - await paired_sensor_manager.async_process_latest_paired_sensor_uids() + paired_sensor_manager = PairedSensorManager( + hass, + entry, + client, + api_lock, + valve_controller_coordinators[API_SENSOR_PAIR_DUMP], + ) + await paired_sensor_manager.async_initialize() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_CLIENT: client, - DATA_COORDINATOR: coordinators, - DATA_COORDINATOR_PAIRED_SENSOR: {}, - DATA_PAIRED_SENSOR_MANAGER: paired_sensor_manager, - } - - @callback - def async_process_paired_sensor_uids() -> None: - """Define a callback for when new paired sensor data is received.""" - hass.async_create_task( - paired_sensor_manager.async_process_latest_paired_sensor_uids() - ) - - coordinators[API_SENSOR_PAIR_DUMP].async_add_listener( - async_process_paired_sensor_uids + hass.data[DOMAIN][entry.entry_id] = GuardianData( + entry=entry, + client=client, + valve_controller_coordinators=valve_controller_coordinators, + paired_sensor_manager=paired_sensor_manager, ) # Set up all of the Guardian entity platforms: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback - def hydrate_with_entry_and_client(func: Callable) -> Callable: - """Define a decorator to hydrate a method with args based on service call.""" + def call_with_data(func: Callable) -> Callable: + """Hydrate a service call with the appropriate GuardianData object.""" async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" entry_id = async_get_entry_id_for_service_call(hass, call) - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - entry = hass.config_entries.async_get_entry(entry_id) - assert entry + data = hass.data[DOMAIN][entry_id] try: - async with client: - await func(call, entry, client) + async with data.client: + await func(call, data) except GuardianError as err: raise HomeAssistantError( f"Error while executing {func.__name__}: {err}" @@ -200,78 +202,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper - @hydrate_with_entry_and_client - async def async_disable_ap( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_disable_ap(call: ServiceCall, data: GuardianData) -> None: """Disable the onboard AP.""" - await client.wifi.disable_ap() + await data.client.wifi.disable_ap() - @hydrate_with_entry_and_client - async def async_enable_ap( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_enable_ap(call: ServiceCall, data: GuardianData) -> None: """Enable the onboard AP.""" - await client.wifi.enable_ap() + await data.client.wifi.enable_ap() - @hydrate_with_entry_and_client - async def async_pair_sensor( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_pair_sensor(call: ServiceCall, data: GuardianData) -> None: """Add a new paired sensor.""" - paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ - DATA_PAIRED_SENSOR_MANAGER - ] uid = call.data[CONF_UID] + await data.client.sensor.pair_sensor(uid) + await data.paired_sensor_manager.async_pair_sensor(uid) - await client.sensor.pair_sensor(uid) - await paired_sensor_manager.async_pair_sensor(uid) - - @hydrate_with_entry_and_client - async def async_reboot( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_reboot(call: ServiceCall, data: GuardianData) -> None: """Reboot the valve controller.""" async_log_deprecated_service_call( hass, call, "button.press", - f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reboot", + f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reboot", ) - await client.system.reboot() + await data.client.system.reboot() - @hydrate_with_entry_and_client + @call_with_data async def async_reset_valve_diagnostics( - call: ServiceCall, entry: ConfigEntry, client: Client + call: ServiceCall, data: GuardianData ) -> None: """Fully reset system motor diagnostics.""" async_log_deprecated_service_call( hass, call, "button.press", - f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reset_valve_diagnostics", + f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reset_valve_diagnostics", ) - await client.valve.reset() + await data.client.valve.reset() - @hydrate_with_entry_and_client - async def async_unpair_sensor( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_unpair_sensor(call: ServiceCall, data: GuardianData) -> None: """Remove a paired sensor.""" - paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ - DATA_PAIRED_SENSOR_MANAGER - ] uid = call.data[CONF_UID] + await data.client.sensor.unpair_sensor(uid) + await data.paired_sensor_manager.async_unpair_sensor(uid) - await client.sensor.unpair_sensor(uid) - await paired_sensor_manager.async_unpair_sensor(uid) - - @hydrate_with_entry_and_client - async def async_upgrade_firmware( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_upgrade_firmware(call: ServiceCall, data: GuardianData) -> None: """Upgrade the device firmware.""" - await client.system.upgrade_firmware( + await data.client.system.upgrade_firmware( url=call.data[CONF_URL], port=call.data[CONF_PORT], filename=call.data[CONF_FILENAME], @@ -338,6 +320,7 @@ class PairedSensorManager: entry: ConfigEntry, client: Client, api_lock: asyncio.Lock, + sensor_pair_dump_coordinator: GuardianDataUpdateCoordinator, ) -> None: """Initialize.""" self._api_lock = api_lock @@ -345,6 +328,21 @@ class PairedSensorManager: self._entry = entry self._hass = hass self._paired_uids: set[str] = set() + self._sensor_pair_dump_coordinator = sensor_pair_dump_coordinator + self.coordinators: dict[str, GuardianDataUpdateCoordinator] = {} + + async def async_initialize(self) -> None: + """Initialize the manager.""" + + @callback + def async_create_process_task() -> None: + """Define a callback for when new paired sensor data is received.""" + self._hass.async_create_task(self.async_process_latest_paired_sensor_uids()) + + cancel_process_task = self._sensor_pair_dump_coordinator.async_add_listener( + async_create_process_task + ) + self._entry.async_on_unload(cancel_process_task) async def async_pair_sensor(self, uid: str) -> None: """Add a new paired sensor coordinator.""" @@ -352,9 +350,7 @@ class PairedSensorManager: self._paired_uids.add(uid) - coordinator = self._hass.data[DOMAIN][self._entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ][uid] = GuardianDataUpdateCoordinator( + coordinator = self.coordinators[uid] = GuardianDataUpdateCoordinator( self._hass, client=self._client, api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}", @@ -375,11 +371,7 @@ class PairedSensorManager: async def async_process_latest_paired_sensor_uids(self) -> None: """Process a list of new UIDs.""" try: - uids = set( - self._hass.data[DOMAIN][self._entry.entry_id][DATA_COORDINATOR][ - API_SENSOR_PAIR_DUMP - ].data["paired_uids"] - ) + uids = set(self._sensor_pair_dump_coordinator.data["paired_uids"]) except KeyError: # Sometimes the paired_uids key can fail to exist; the user can't do anything # about it, so in this case, we quietly abort and return: @@ -403,9 +395,7 @@ class PairedSensorManager: # Clear out objects related to this paired sensor: self._paired_uids.remove(uid) - self._hass.data[DOMAIN][self._entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ].pop(uid) + self.coordinators.pop(uid) # Remove the paired sensor device from the device registry (which will # clean up entities and the entity registry): diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 9b824ab589f..eb6d49c3ec1 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -15,6 +15,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( + GuardianData, PairedSensorEntity, ValveControllerEntity, ValveControllerEntityDescription, @@ -23,8 +24,6 @@ from .const import ( API_SYSTEM_ONBOARD_SENSOR_STATUS, API_WIFI_STATUS, CONF_UID, - DATA_COORDINATOR, - DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) @@ -79,16 +78,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" - entry_data = hass.data[DOMAIN][entry.entry_id] - paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR] - valve_controller_coordinators = entry_data[DATA_COORDINATOR] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" async_add_entities( PairedSensorBinarySensor( - entry, paired_sensor_coordinators[uid], description + entry, data.paired_sensor_manager.coordinators[uid], description ) for description in PAIRED_SENSOR_DESCRIPTIONS ) @@ -104,7 +101,9 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [ - ValveControllerBinarySensor(entry, valve_controller_coordinators, description) + ValveControllerBinarySensor( + entry, data.valve_controller_coordinators, description + ) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -112,7 +111,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorBinarySensor(entry, coordinator, description) - for coordinator in paired_sensor_coordinators.values() + for coordinator in data.paired_sensor_manager.coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index e013cde85d6..01efb7deba4 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -18,9 +18,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ValveControllerEntity, ValveControllerEntityDescription -from .const import API_SYSTEM_DIAGNOSTICS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN -from .util import GuardianDataUpdateCoordinator +from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription +from .const import API_SYSTEM_DIAGNOSTICS, DOMAIN @dataclass @@ -77,13 +76,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian buttons based on a config entry.""" - entry_data = hass.data[DOMAIN][entry.entry_id] - client = entry_data[DATA_CLIENT] - valve_controller_coordinators = entry_data[DATA_COORDINATOR] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - GuardianButton(entry, valve_controller_coordinators, description, client) - for description in BUTTON_DESCRIPTIONS + GuardianButton(entry, data, description) for description in BUTTON_DESCRIPTIONS ) @@ -98,14 +94,13 @@ class GuardianButton(ValveControllerEntity, ButtonEntity): def __init__( self, entry: ConfigEntry, - coordinators: dict[str, GuardianDataUpdateCoordinator], + data: GuardianData, description: ValveControllerButtonDescription, - client: Client, ) -> None: """Initialize.""" - super().__init__(entry, coordinators, description) + super().__init__(entry, data.valve_controller_coordinators, description) - self._client = client + self._client = data.client async def async_press(self) -> None: """Send out a restart command.""" diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index 3499db24c03..c7d025ba712 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -14,8 +14,4 @@ API_WIFI_STATUS = "wifi_status" CONF_UID = "uid" -DATA_CLIENT = "client" -DATA_COORDINATOR = "coordinator" -DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor" - SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED = "guardian_paired_sensor_coordinator_added_{0}" diff --git a/homeassistant/components/guardian/diagnostics.py b/homeassistant/components/guardian/diagnostics.py index 175136b33f4..d53dcb68fa8 100644 --- a/homeassistant/components/guardian/diagnostics.py +++ b/homeassistant/components/guardian/diagnostics.py @@ -7,8 +7,8 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import CONF_UID, DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN -from .util import GuardianDataUpdateCoordinator +from . import GuardianData +from .const import CONF_UID, DOMAIN CONF_BSSID = "bssid" CONF_PAIRED_UIDS = "paired_uids" @@ -26,12 +26,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = hass.data[DOMAIN][entry.entry_id] - - coordinators: dict[str, GuardianDataUpdateCoordinator] = data[DATA_COORDINATOR] - paired_sensor_coordinators: dict[str, GuardianDataUpdateCoordinator] = data[ - DATA_COORDINATOR_PAIRED_SENSOR - ] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] return { "entry": { @@ -41,11 +36,11 @@ async def async_get_config_entry_diagnostics( "data": { "valve_controller": { api_category: async_redact_data(coordinator.data, TO_REDACT) - for api_category, coordinator in coordinators.items() + for api_category, coordinator in data.valve_controller_coordinators.items() }, "paired_sensors": [ async_redact_data(coordinator.data, TO_REDACT) - for coordinator in paired_sensor_coordinators.values() + for coordinator in data.paired_sensor_manager.coordinators.values() ], }, } diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index c4fd1e110fa..bf7d9e7122a 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -17,6 +17,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( + GuardianData, PairedSensorEntity, ValveControllerEntity, ValveControllerEntityDescription, @@ -25,8 +26,6 @@ from .const import ( API_SYSTEM_DIAGNOSTICS, API_SYSTEM_ONBOARD_SENSOR_STATUS, CONF_UID, - DATA_COORDINATOR, - DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) @@ -83,15 +82,15 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" - entry_data = hass.data[DOMAIN][entry.entry_id] - paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR] - valve_controller_coordinators = entry_data[DATA_COORDINATOR] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" async_add_entities( - PairedSensorSensor(entry, paired_sensor_coordinators[uid], description) + PairedSensorSensor( + entry, data.paired_sensor_manager.coordinators[uid], description + ) for description in PAIRED_SENSOR_DESCRIPTIONS ) @@ -106,7 +105,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorSensor | ValveControllerSensor] = [ - ValveControllerSensor(entry, valve_controller_coordinators, description) + ValveControllerSensor(entry, data.valve_controller_coordinators, description) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -114,7 +113,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorSensor(entry, coordinator, description) - for coordinator in paired_sensor_coordinators.values() + for coordinator in data.paired_sensor_manager.coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index c58f4548a87..4e100ce4fe4 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from aioguardian import Client from aioguardian.errors import GuardianError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -13,9 +12,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ValveControllerEntity, ValveControllerEntityDescription -from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN -from .util import GuardianDataUpdateCoordinator +from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription +from .const import API_VALVE_STATUS, DOMAIN ATTR_AVG_CURRENT = "average_current" ATTR_INST_CURRENT = "instantaneous_current" @@ -46,12 +44,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" - entry_data = hass.data[DOMAIN][entry.entry_id] - client = entry_data[DATA_CLIENT] - valve_controller_coordinators = entry_data[DATA_COORDINATOR] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - ValveControllerSwitch(entry, valve_controller_coordinators, description, client) + ValveControllerSwitch(entry, data, description) for description in VALVE_CONTROLLER_DESCRIPTIONS ) @@ -71,15 +67,14 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): def __init__( self, entry: ConfigEntry, - coordinators: dict[str, GuardianDataUpdateCoordinator], + data: GuardianData, description: ValveControllerSwitchDescription, - client: Client, ) -> None: """Initialize.""" - super().__init__(entry, coordinators, description) + super().__init__(entry, data.valve_controller_coordinators, description) self._attr_is_on = True - self._client = client + self._client = data.client @callback def _async_update_from_latest_data(self) -> None: diff --git a/tests/components/guardian/test_diagnostics.py b/tests/components/guardian/test_diagnostics.py index f48c988c907..2269d09b1eb 100644 --- a/tests/components/guardian/test_diagnostics.py +++ b/tests/components/guardian/test_diagnostics.py @@ -1,22 +1,16 @@ """Test Guardian diagnostics.""" from homeassistant.components.diagnostics import REDACTED -from homeassistant.components.guardian import ( - DATA_PAIRED_SENSOR_MANAGER, - DOMAIN, - PairedSensorManager, -) +from homeassistant.components.guardian import DOMAIN, GuardianData from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics(hass, config_entry, hass_client, setup_guardian): """Test config entry diagnostics.""" - paired_sensor_manager: PairedSensorManager = hass.data[DOMAIN][ - config_entry.entry_id - ][DATA_PAIRED_SENSOR_MANAGER] + data: GuardianData = hass.data[DOMAIN][config_entry.entry_id] # Simulate the pairing of a paired sensor: - await paired_sensor_manager.async_pair_sensor("AABBCCDDEEFF") + await data.paired_sensor_manager.async_pair_sensor("AABBCCDDEEFF") assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { From 06115bcbff2900366e09e5c9393b11d62b4242f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 22:17:09 -0500 Subject: [PATCH 621/820] Add inkbird (BLE) integration (#75594) --- CODEOWNERS | 2 + homeassistant/components/inkbird/__init__.py | 56 +++++++ .../components/inkbird/config_flow.py | 93 ++++++++++++ homeassistant/components/inkbird/const.py | 3 + .../components/inkbird/manifest.json | 16 ++ homeassistant/components/inkbird/sensor.py | 142 ++++++++++++++++++ homeassistant/components/inkbird/strings.json | 21 +++ .../components/inkbird/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 16 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/inkbird/__init__.py | 36 +++++ tests/components/inkbird/conftest.py | 8 + tests/components/inkbird/test_config_flow.py | 94 ++++++++++++ tests/components/inkbird/test_sensor.py | 50 ++++++ 16 files changed, 565 insertions(+) create mode 100644 homeassistant/components/inkbird/__init__.py create mode 100644 homeassistant/components/inkbird/config_flow.py create mode 100644 homeassistant/components/inkbird/const.py create mode 100644 homeassistant/components/inkbird/manifest.json create mode 100644 homeassistant/components/inkbird/sensor.py create mode 100644 homeassistant/components/inkbird/strings.json create mode 100644 homeassistant/components/inkbird/translations/en.json create mode 100644 tests/components/inkbird/__init__.py create mode 100644 tests/components/inkbird/conftest.py create mode 100644 tests/components/inkbird/test_config_flow.py create mode 100644 tests/components/inkbird/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 2a3999779fc..792a6302b79 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -498,6 +498,8 @@ build.json @home-assistant/supervisor /homeassistant/components/incomfort/ @zxdavb /homeassistant/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 +/homeassistant/components/inkbird/ @bdraco +/tests/components/inkbird/ @bdraco /homeassistant/components/input_boolean/ @home-assistant/core /tests/components/input_boolean/ @home-assistant/core /homeassistant/components/input_button/ @home-assistant/core diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py new file mode 100644 index 00000000000..29b63e34263 --- /dev/null +++ b/homeassistant/components/inkbird/__init__.py @@ -0,0 +1,56 @@ +"""The INKBIRD Bluetooth integration.""" +from __future__ import annotations + +import logging + +from inkbird_ble import INKBIRDBluetoothDeviceData + +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from .const import DOMAIN +from .sensor import sensor_update_to_bluetooth_data_update + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up INKBIRD BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + + data = INKBIRDBluetoothDeviceData() + + @callback + def _async_update_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Update data from INKBIRD Bluetooth.""" + return sensor_update_to_bluetooth_data_update(data.update(service_info)) + + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothDataUpdateCoordinator( + hass, + _LOGGER, + update_method=_async_update_data, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py new file mode 100644 index 00000000000..679ff43b19e --- /dev/null +++ b/homeassistant/components/inkbird/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for inkbird ble integration.""" +from __future__ import annotations + +from typing import Any + +from inkbird_ble import INKBIRDBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for inkbird.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/inkbird/const.py b/homeassistant/components/inkbird/const.py new file mode 100644 index 00000000000..9d0e1638958 --- /dev/null +++ b/homeassistant/components/inkbird/const.py @@ -0,0 +1,3 @@ +"""Constants for the INKBIRD Bluetooth integration.""" + +DOMAIN = "inkbird" diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json new file mode 100644 index 00000000000..686c9bada2d --- /dev/null +++ b/homeassistant/components/inkbird/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "inkbird", + "name": "INKBIRD", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/inkbird", + "bluetooth": [ + { "local_name": "sps" }, + { "local_name": "Inkbird*" }, + { "local_name": "iBBQ*" }, + { "local_name": "tps" } + ], + "requirements": ["inkbird-ble==0.5.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py new file mode 100644 index 00000000000..26311131181 --- /dev/null +++ b/homeassistant/components/inkbird/sensor.py @@ -0,0 +1,142 @@ +"""Support for inkbird ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, + PassiveBluetoothEntityKey, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the INKBIRD BLE sensors.""" + coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + entry.async_on_unload( + coordinator.async_add_entities_listener( + INKBIRDBluetoothSensorEntity, async_add_entities + ) + ) + + +class INKBIRDBluetoothSensorEntity( + PassiveBluetoothCoordinatorEntity[ + PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a inkbird ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.coordinator.entity_data.get(self.entity_key) diff --git a/homeassistant/components/inkbird/strings.json b/homeassistant/components/inkbird/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/inkbird/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/inkbird/translations/en.json b/homeassistant/components/inkbird/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/inkbird/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index e3a4edc11d4..1edd25a7609 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -14,6 +14,22 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ 6 ] }, + { + "domain": "inkbird", + "local_name": "sps" + }, + { + "domain": "inkbird", + "local_name": "Inkbird*" + }, + { + "domain": "inkbird", + "local_name": "iBBQ*" + }, + { + "domain": "inkbird", + "local_name": "tps" + }, { "domain": "sensorpush", "local_name": "SensorPush*" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 95050ea6dbd..33e34035b34 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -166,6 +166,7 @@ FLOWS = { "iaqualink", "icloud", "ifttt", + "inkbird", "insteon", "intellifire", "ios", diff --git a/requirements_all.txt b/requirements_all.txt index f125a49c0a4..0b3c0e4b370 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -896,6 +896,9 @@ influxdb-client==1.24.0 # homeassistant.components.influxdb influxdb==5.3.1 +# homeassistant.components.inkbird +inkbird-ble==0.5.1 + # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 605eb8352ea..53f1689e476 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,6 +648,9 @@ influxdb-client==1.24.0 # homeassistant.components.influxdb influxdb==5.3.1 +# homeassistant.components.inkbird +inkbird-ble==0.5.1 + # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/tests/components/inkbird/__init__.py b/tests/components/inkbird/__init__.py new file mode 100644 index 00000000000..0a74e50ae0e --- /dev/null +++ b/tests/components/inkbird/__init__.py @@ -0,0 +1,36 @@ +"""Tests for the INKBIRD integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_INKBIRD_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +SPS_SERVICE_INFO = BluetoothServiceInfo( + name="sps", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + service_data={}, + manufacturer_data={2096: b"\x0f\x12\x00Z\xc7W\x06"}, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + source="local", +) + +IBBQ_SERVICE_INFO = BluetoothServiceInfo( + name="iBBQ", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={ + 0: b"\x00\x000\xe2\x83}\xb5\x02\xc8\x00\xc8\x00\xc8\x00\xc8\x00" + }, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) diff --git a/tests/components/inkbird/conftest.py b/tests/components/inkbird/conftest.py new file mode 100644 index 00000000000..c44e9f7929a --- /dev/null +++ b/tests/components/inkbird/conftest.py @@ -0,0 +1,8 @@ +"""INKBIRD session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(mock_bleak_scanner_start): + """Auto mock bluetooth.""" diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py new file mode 100644 index 00000000000..b783562b126 --- /dev/null +++ b/tests/components/inkbird/test_config_flow.py @@ -0,0 +1,94 @@ +"""Test the INKBIRD config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.inkbird.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import IBBQ_SERVICE_INFO, NOT_INKBIRD_SERVICE_INFO, SPS_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=IBBQ_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "iBBQ 6AADDD4CAC3D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_bluetooth_not_inkbird(hass): + """Test discovery via bluetooth not inkbird.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_INKBIRD_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py new file mode 100644 index 00000000000..80a2179666f --- /dev/null +++ b/tests/components/inkbird/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the INKBIRD config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.inkbird.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import SPS_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(SPS_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.ibs_th_75bbe1738105_battery") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "87" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH 75BBE1738105 Battery" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From a612d7a0f31f2a5835ebdede77d06015ff0c8578 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 22 Jul 2022 11:46:00 +0800 Subject: [PATCH 622/820] Round up for stream record lookback (#75580) --- homeassistant/components/stream/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index f0b4ed99654..354f9a77672 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -503,15 +503,16 @@ class Stream: await self.start() + self._logger.debug("Started a stream recording of %s seconds", duration) + # Take advantage of lookback hls: HlsStreamOutput = cast(HlsStreamOutput, self.outputs().get(HLS_PROVIDER)) - if lookback > 0 and hls: - num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) + if hls: + num_segments = min(int(lookback / hls.target_duration) + 1, MAX_SEGMENTS) # Wait for latest segment, then add the lookback await hls.recv() recorder.prepend(list(hls.get_segments())[-num_segments - 1 : -1]) - self._logger.debug("Started a stream recording of %s seconds", duration) await recorder.async_record() async def async_get_image( From dddd4e24e29566ddf5f296f7f421885151268517 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Fri, 22 Jul 2022 11:11:31 +0200 Subject: [PATCH 623/820] Bump afsapi to 0.2.7 (#75579) --- homeassistant/components/frontier_silicon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index b04d68e672d..1abce3b9a60 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.2.6"], + "requirements": ["afsapi==0.2.7"], "codeowners": ["@wlcrs"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0b3c0e4b370..927207a8eeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,7 +89,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.2.6 +afsapi==0.2.7 # homeassistant.components.agent_dvr agent-py==0.0.23 From f0eea62c1eeb8b7e7cd319f885ad5c849eda8796 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Jul 2022 11:12:00 +0200 Subject: [PATCH 624/820] Address some MQTT review comments (#75482) --- .../mqtt/device_tracker/__init__.py | 13 +++++++-- .../mqtt/device_tracker/schema_yaml.py | 29 ++++++++++++++----- homeassistant/components/mqtt/mixins.py | 2 +- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index 99e0c87044b..342817e38cc 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -10,7 +10,11 @@ from ..const import MQTT_DATA_DEVICE_TRACKER_LEGACY from ..mixins import warn_for_legacy_schema from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401 from .schema_discovery import async_setup_entry_from_discovery -from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml +from .schema_yaml import ( + PLATFORM_SCHEMA_YAML, + MQTTLegacyDeviceTrackerData, + async_setup_scanner_from_yaml, +) # Configuring MQTT Device Trackers under the device_tracker platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( @@ -30,6 +34,11 @@ async def async_setup_entry( await async_setup_entry_from_discovery(hass, config_entry, async_add_entities) # (re)load legacy service if MQTT_DATA_DEVICE_TRACKER_LEGACY in hass.data: + yaml_device_tracker_data: MQTTLegacyDeviceTrackerData = hass.data[ + MQTT_DATA_DEVICE_TRACKER_LEGACY + ] await async_setup_scanner_from_yaml( - hass, **hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] + hass, + config=yaml_device_tracker_data.config, + async_see=yaml_device_tracker_data.async_see, ) diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py index 1990b380dcb..c005a82dbeb 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -1,6 +1,8 @@ """Support for tracking MQTT enabled devices defined in YAML.""" +from __future__ import annotations -from collections.abc import Callable +from collections.abc import Awaitable, Callable +import dataclasses import logging from typing import Any @@ -10,6 +12,7 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ... import mqtt from ..client import async_subscribe @@ -33,9 +36,20 @@ PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend( ) +@dataclasses.dataclass +class MQTTLegacyDeviceTrackerData: + """Class to hold device tracker data.""" + + async_see: Callable[..., Awaitable[None]] + config: ConfigType + + async def async_setup_scanner_from_yaml( - hass: HomeAssistant, config, async_see, discovery_info=None -): + hass: HomeAssistant, + config: ConfigType, + async_see: Callable[..., Awaitable[None]], + discovery_info: DiscoveryInfoType | None = None, +) -> bool: """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] @@ -45,15 +59,14 @@ async def async_setup_scanner_from_yaml( config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] subscriptions: list[Callable] = [] - hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] = { - "async_see": async_see, - "config": config, - } + hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] = MQTTLegacyDeviceTrackerData( + async_see, config + ) if not mqtt_config_entry_enabled(hass): _LOGGER.info( "MQTT device trackers will be not available until the config entry is enabled", ) - return + return False @callback def _entry_unload(*_: Any) -> None: diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index af0660b4c93..f0cc0c4ad44 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -674,7 +674,7 @@ class MqttDiscoveryDeviceUpdate: stop_discovery_updates( self.hass, self._discovery_data, self._remove_discovery_updated ) - self.hass.async_add_job(self.async_tear_down()) + self._config_entry.async_create_task(self.hass, self.async_tear_down()) async def async_discovery_update( self, From 630c28d253035eb0b141599f1707d0b6b20c97d1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 22 Jul 2022 03:24:07 -0600 Subject: [PATCH 625/820] Fix incorrect battery unit on paired Guardian sensors (#75402) --- homeassistant/components/guardian/manifest.json | 2 +- homeassistant/components/guardian/sensor.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index fe9a453a166..7fab487563c 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,7 +3,7 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "requirements": ["aioguardian==2022.03.2"], + "requirements": ["aioguardian==2022.07.0"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], "iot_class": "local_polling", diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index bf7d9e7122a..5b4c621cce6 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_FAHRENHEIT, TIME_MINUTES +from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, TEMP_FAHRENHEIT, TIME_MINUTES from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory @@ -46,9 +46,9 @@ PAIRED_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_BATTERY, name="Battery", - device_class=SensorDeviceClass.BATTERY, + device_class=SensorDeviceClass.VOLTAGE, entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, ), SensorEntityDescription( key=SENSOR_KIND_TEMPERATURE, diff --git a/requirements_all.txt b/requirements_all.txt index 927207a8eeb..c17e031f07a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -162,7 +162,7 @@ aioftp==0.12.0 aiogithubapi==22.2.4 # homeassistant.components.guardian -aioguardian==2022.03.2 +aioguardian==2022.07.0 # homeassistant.components.harmony aioharmony==0.2.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53f1689e476..8745afe1e64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aioflo==2021.11.0 aiogithubapi==22.2.4 # homeassistant.components.guardian -aioguardian==2022.03.2 +aioguardian==2022.07.0 # homeassistant.components.harmony aioharmony==0.2.9 From 06c8eb0304ae1ce422571aaa34b35c507b802594 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 22 Jul 2022 11:57:36 +0200 Subject: [PATCH 626/820] Migrate SMHI to new entity naming style (#75213) --- homeassistant/components/smhi/weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index d7df54957c0..f8afcb7b59a 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -126,6 +126,8 @@ class SmhiWeather(WeatherEntity): _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND _attr_native_pressure_unit = PRESSURE_HPA + _attr_has_entity_name = True + def __init__( self, name: str, @@ -134,8 +136,6 @@ class SmhiWeather(WeatherEntity): session: aiohttp.ClientSession, ) -> None: """Initialize the SMHI weather entity.""" - - self._attr_name = name self._attr_unique_id = f"{latitude}, {longitude}" self._forecasts: list[SmhiForecast] | None = None self._fail_count = 0 From 606d5441573f52de961010551b34d23aef3317dc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Jul 2022 11:58:26 +0200 Subject: [PATCH 627/820] Use recorder get_instance function to improve typing (#75567) --- homeassistant/components/recorder/__init__.py | 11 ++--- homeassistant/components/recorder/backup.py | 8 ++-- .../components/recorder/statistics.py | 7 +-- homeassistant/components/recorder/util.py | 10 +++- .../components/recorder/websocket_api.py | 24 ++++------ tests/components/recorder/common.py | 5 +- tests/components/recorder/test_backup.py | 8 ++-- tests/components/recorder/test_init.py | 46 ++++++++++--------- tests/components/recorder/test_migrate.py | 11 ++--- tests/components/recorder/test_statistics.py | 6 +-- tests/components/recorder/test_util.py | 16 +++---- .../components/recorder/test_websocket_api.py | 3 +- tests/components/sensor/test_recorder.py | 7 ++- 13 files changed, 77 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 4063e443e8b..238b013f366 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -31,6 +31,7 @@ from .const import ( from .core import Recorder from .services import async_register_services from .tasks import AddRecorderPlatformTask +from .util import get_instance _LOGGER = logging.getLogger(__name__) @@ -108,12 +109,6 @@ CONFIG_SCHEMA = vol.Schema( ) -def get_instance(hass: HomeAssistant) -> Recorder: - """Get the recorder instance.""" - instance: Recorder = hass.data[DATA_INSTANCE] - return instance - - @bind_hass def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """Check if an entity is being recorded. @@ -122,7 +117,7 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """ if DATA_INSTANCE not in hass.data: return False - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) return instance.entity_filter(entity_id) @@ -177,5 +172,5 @@ async def _process_recorder_platform( hass: HomeAssistant, domain: str, platform: Any ) -> None: """Process a recorder platform.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) instance.queue_task(AddRecorderPlatformTask(domain, platform)) diff --git a/homeassistant/components/recorder/backup.py b/homeassistant/components/recorder/backup.py index cec9f85748b..a1f6f4f39bc 100644 --- a/homeassistant/components/recorder/backup.py +++ b/homeassistant/components/recorder/backup.py @@ -4,9 +4,7 @@ from logging import getLogger from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import Recorder -from .const import DATA_INSTANCE -from .util import async_migration_in_progress +from .util import async_migration_in_progress, get_instance _LOGGER = getLogger(__name__) @@ -14,7 +12,7 @@ _LOGGER = getLogger(__name__) async def async_pre_backup(hass: HomeAssistant) -> None: """Perform operations before a backup starts.""" _LOGGER.info("Backup start notification, locking database for writes") - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) if async_migration_in_progress(hass): raise HomeAssistantError("Database migration in progress") await instance.lock_database() @@ -22,7 +20,7 @@ async def async_pre_backup(hass: HomeAssistant) -> None: async def async_post_backup(hass: HomeAssistant) -> None: """Perform operations after a backup finishes.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) _LOGGER.info("Backup end notification, releasing write lock") if not instance.unlock_database(): raise HomeAssistantError("Could not release database write lock") diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4ebd5e17902..bce77e8a31e 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -41,7 +41,7 @@ import homeassistant.util.temperature as temperature_util from homeassistant.util.unit_system import UnitSystem import homeassistant.util.volume as volume_util -from .const import DATA_INSTANCE, DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect +from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect from .db_schema import Statistics, StatisticsMeta, StatisticsRuns, StatisticsShortTerm from .models import ( StatisticData, @@ -53,6 +53,7 @@ from .models import ( from .util import ( execute, execute_stmt_lambda_element, + get_instance, retryable_database_job, session_scope, ) @@ -209,7 +210,7 @@ def async_setup(hass: HomeAssistant) -> None: @callback def _async_entity_id_changed(event: Event) -> None: - hass.data[DATA_INSTANCE].async_update_statistics_metadata( + get_instance(hass).async_update_statistics_metadata( event.data["old_entity_id"], new_statistic_id=event.data["entity_id"] ) @@ -1385,7 +1386,7 @@ def _async_import_statistics( statistic["last_reset"] = dt_util.as_utc(last_reset) # Insert job in recorder's queue - hass.data[DATA_INSTANCE].async_import_statistics(metadata, statistics) + get_instance(hass).async_import_statistics(metadata, statistics) @callback diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c1fbc831987..fdf42665ef5 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -80,7 +80,7 @@ def session_scope( ) -> Generator[Session, None, None]: """Provide a transactional scope around a series of operations.""" if session is None and hass is not None: - session = hass.data[DATA_INSTANCE].get_session() + session = get_instance(hass).get_session() if session is None: raise RuntimeError("Session required") @@ -559,7 +559,7 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: """ if DATA_INSTANCE not in hass.data: return False - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) return instance.migration_in_progress @@ -577,3 +577,9 @@ def second_sunday(year: int, month: int) -> date: def is_second_sunday(date_time: datetime) -> bool: """Check if a time is the second sunday of the month.""" return bool(second_sunday(date_time.year, date_time.month).day == date_time.day) + + +def get_instance(hass: HomeAssistant) -> Recorder: + """Get the recorder instance.""" + instance: Recorder = hass.data[DATA_INSTANCE] + return instance diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 4ba5f3c8a8b..c143d8b4f0b 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING import voluptuous as vol @@ -11,17 +10,14 @@ from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv from homeassistant.util import dt as dt_util -from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG +from .const import MAX_QUEUE_BACKLOG from .statistics import ( async_add_external_statistics, async_import_statistics, list_statistic_ids, validate_statistics, ) -from .util import async_migration_in_progress - -if TYPE_CHECKING: - from . import Recorder +from .util import async_migration_in_progress, get_instance _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -50,7 +46,7 @@ async def ws_validate_statistics( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) statistic_ids = await instance.async_add_executor_job( validate_statistics, hass, @@ -74,7 +70,7 @@ def ws_clear_statistics( Note: The WS call posts a job to the recorder's queue and then returns, it doesn't wait until the job is completed. """ - hass.data[DATA_INSTANCE].async_clear_statistics(msg["statistic_ids"]) + get_instance(hass).async_clear_statistics(msg["statistic_ids"]) connection.send_result(msg["id"]) @@ -89,7 +85,7 @@ async def ws_get_statistics_metadata( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Get metadata for a list of statistic_ids.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) statistic_ids = await instance.async_add_executor_job( list_statistic_ids, hass, msg.get("statistic_ids") ) @@ -109,7 +105,7 @@ def ws_update_statistics_metadata( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Update statistics metadata for a statistic_id.""" - hass.data[DATA_INSTANCE].async_update_statistics_metadata( + get_instance(hass).async_update_statistics_metadata( msg["statistic_id"], new_unit_of_measurement=msg["unit_of_measurement"] ) connection.send_result(msg["id"]) @@ -137,7 +133,7 @@ def ws_adjust_sum_statistics( connection.send_error(msg["id"], "invalid_start_time", "Invalid start time") return - hass.data[DATA_INSTANCE].async_adjust_statistics( + get_instance(hass).async_adjust_statistics( msg["statistic_id"], start_time, msg["adjustment"] ) connection.send_result(msg["id"]) @@ -193,7 +189,7 @@ def ws_info( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Return status of the recorder.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) backlog = instance.backlog if instance else None migration_in_progress = async_migration_in_progress(hass) @@ -219,7 +215,7 @@ async def ws_backup_start( """Backup start notification.""" _LOGGER.info("Backup start notification, locking database for writes") - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) try: await instance.lock_database() except TimeoutError as err: @@ -236,7 +232,7 @@ async def ws_backup_end( ) -> None: """Backup end notification.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) _LOGGER.info("Backup end notification, releasing write lock") if not instance.unlock_database(): connection.send_error( diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 20df89eca5b..083630c7ea8 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -62,7 +62,7 @@ def wait_recording_done(hass: HomeAssistant) -> None: hass.block_till_done() trigger_db_commit(hass) hass.block_till_done() - hass.data[recorder.DATA_INSTANCE].block_till_done() + recorder.get_instance(hass).block_till_done() hass.block_till_done() @@ -105,8 +105,7 @@ def async_trigger_db_commit(hass: HomeAssistant) -> None: async def async_recorder_block_till_done(hass: HomeAssistant) -> None: """Non blocking version of recorder.block_till_done().""" - instance: recorder.Recorder = hass.data[recorder.DATA_INSTANCE] - await hass.async_add_executor_job(instance.block_till_done) + await hass.async_add_executor_job(recorder.get_instance(hass).block_till_done) def corrupt_db_file(test_db_file): diff --git a/tests/components/recorder/test_backup.py b/tests/components/recorder/test_backup.py index d7c5a55b56a..e829c2aa13b 100644 --- a/tests/components/recorder/test_backup.py +++ b/tests/components/recorder/test_backup.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError async def test_async_pre_backup(hass: HomeAssistant, recorder_mock) -> None: """Test pre backup.""" with patch( - "homeassistant.components.recorder.backup.Recorder.lock_database" + "homeassistant.components.recorder.core.Recorder.lock_database" ) as lock_mock: await async_pre_backup(hass) assert lock_mock.called @@ -24,7 +24,7 @@ async def test_async_pre_backup_with_timeout( ) -> None: """Test pre backup with timeout.""" with patch( - "homeassistant.components.recorder.backup.Recorder.lock_database", + "homeassistant.components.recorder.core.Recorder.lock_database", side_effect=TimeoutError(), ) as lock_mock, pytest.raises(TimeoutError): await async_pre_backup(hass) @@ -45,7 +45,7 @@ async def test_async_pre_backup_with_migration( async def test_async_post_backup(hass: HomeAssistant, recorder_mock) -> None: """Test post backup.""" with patch( - "homeassistant.components.recorder.backup.Recorder.unlock_database" + "homeassistant.components.recorder.core.Recorder.unlock_database" ) as unlock_mock: await async_post_backup(hass) assert unlock_mock.called @@ -54,7 +54,7 @@ async def test_async_post_backup(hass: HomeAssistant, recorder_mock) -> None: async def test_async_post_backup_failure(hass: HomeAssistant, recorder_mock) -> None: """Test post backup failure.""" with patch( - "homeassistant.components.recorder.backup.Recorder.unlock_database", + "homeassistant.components.recorder.core.Recorder.unlock_database", return_value=False, ) as unlock_mock, pytest.raises(HomeAssistantError): await async_post_backup(hass) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 3e25a54e39d..82444f86a05 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -24,7 +24,7 @@ from homeassistant.components.recorder import ( Recorder, get_instance, ) -from homeassistant.components.recorder.const import DATA_INSTANCE, KEEPALIVE_TIME +from homeassistant.components.recorder.const import KEEPALIVE_TIME from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, EventData, @@ -100,13 +100,13 @@ async def test_shutdown_before_startup_finishes( } hass.state = CoreState.not_running - await async_setup_recorder_instance(hass, config) - await hass.data[DATA_INSTANCE].async_db_ready + instance = await async_setup_recorder_instance(hass, config) + await instance.async_db_ready await hass.async_block_till_done() - session = await hass.async_add_executor_job(hass.data[DATA_INSTANCE].get_session) + session = await hass.async_add_executor_job(instance.get_session) - with patch.object(hass.data[DATA_INSTANCE], "engine"): + with patch.object(instance, "engine"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_stop() @@ -214,14 +214,16 @@ async def test_saving_many_states( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test we expire after many commits.""" - await async_setup_recorder_instance(hass, {recorder.CONF_COMMIT_INTERVAL: 0}) + instance = await async_setup_recorder_instance( + hass, {recorder.CONF_COMMIT_INTERVAL: 0} + ) entity_id = "test.recorder" attributes = {"test_attr": 5, "test_attr_10": "nice"} - with patch.object( - hass.data[DATA_INSTANCE].event_session, "expire_all" - ) as expire_all, patch.object(recorder.core, "EXPIRE_AFTER_COMMITS", 2): + with patch.object(instance.event_session, "expire_all") as expire_all, patch.object( + recorder.core, "EXPIRE_AFTER_COMMITS", 2 + ): for _ in range(3): hass.states.async_set(entity_id, "on", attributes) await async_wait_recording_done(hass) @@ -269,14 +271,14 @@ def test_saving_state_with_exception(hass, hass_recorder, caplog): attributes = {"test_attr": 5, "test_attr_10": "nice"} def _throw_if_state_in_session(*args, **kwargs): - for obj in hass.data[DATA_INSTANCE].event_session: + for obj in get_instance(hass).event_session: if isinstance(obj, States): raise OperationalError( "insert the state", "fake params", "forced to fail" ) with patch("time.sleep"), patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "flush", side_effect=_throw_if_state_in_session, ): @@ -307,14 +309,14 @@ def test_saving_state_with_sqlalchemy_exception(hass, hass_recorder, caplog): attributes = {"test_attr": 5, "test_attr_10": "nice"} def _throw_if_state_in_session(*args, **kwargs): - for obj in hass.data[DATA_INSTANCE].event_session: + for obj in get_instance(hass).event_session: if isinstance(obj, States): raise SQLAlchemyError( "insert the state", "fake params", "forced to fail" ) with patch("time.sleep"), patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "flush", side_effect=_throw_if_state_in_session, ): @@ -390,7 +392,7 @@ def test_saving_event(hass, hass_recorder): assert len(events) == 1 event: Event = events[0] - hass.data[DATA_INSTANCE].block_till_done() + get_instance(hass).block_till_done() events: list[Event] = [] with session_scope(hass=hass) as session: @@ -421,7 +423,7 @@ def test_saving_event(hass, hass_recorder): def test_saving_state_with_commit_interval_zero(hass_recorder): """Test saving a state with a commit interval of zero.""" hass = hass_recorder({"commit_interval": 0}) - assert hass.data[DATA_INSTANCE].commit_interval == 0 + get_instance(hass).commit_interval == 0 entity_id = "test.recorder" state = "restoring_from_db" @@ -690,7 +692,7 @@ def run_tasks_at_time(hass, test_time): """Advance the clock and wait for any callbacks to finish.""" fire_time_changed(hass, test_time) hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() + get_instance(hass).block_till_done() @pytest.mark.parametrize("enable_nightly_purge", [True]) @@ -1258,7 +1260,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): sqlite3_exception.__cause__ = sqlite3.DatabaseError() with patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "close", side_effect=OperationalError("statement", {}, []), ): @@ -1267,7 +1269,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): await async_wait_recording_done(hass) with patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "commit", side_effect=[sqlite3_exception, None], ): @@ -1357,7 +1359,7 @@ async def test_database_lock_and_unlock( with session_scope(hass=hass) as session: return list(session.query(Events).filter_by(event_type=event_type)) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) assert await instance.lock_database() @@ -1399,7 +1401,7 @@ async def test_database_lock_and_overflow( with session_scope(hass=hass) as session: return list(session.query(Events).filter_by(event_type=event_type)) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) with patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1), patch.object( recorder.core, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.1 @@ -1424,7 +1426,7 @@ async def test_database_lock_timeout(hass, recorder_mock): """Test locking database timeout when recorder stopped.""" hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) class BlockQueue(recorder.tasks.RecorderTask): event: threading.Event = threading.Event() @@ -1447,7 +1449,7 @@ async def test_database_lock_without_instance(hass, recorder_mock): """Test database lock doesn't fail if instance is not initialized.""" hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) with patch.object(instance, "engine", None): try: assert await instance.lock_database() diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 38d6a191809..a57bd246f8b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -21,7 +21,6 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import db_schema, migration -from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, RecorderRuns, @@ -82,7 +81,7 @@ async def test_migration_in_progress(hass): await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) - await hass.data[DATA_INSTANCE].async_migration_event.wait() + await recorder.get_instance(hass).async_migration_event.wait() assert recorder.util.async_migration_in_progress(hass) is True await async_wait_recording_done(hass) @@ -112,7 +111,7 @@ async def test_database_migration_failed(hass): hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_add_executor_job(recorder.get_instance(hass).join) await hass.async_block_till_done() assert recorder.util.async_migration_in_progress(hass) is False @@ -172,7 +171,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_add_executor_job(recorder.get_instance(hass).join) await hass.async_block_till_done() assert recorder.util.async_migration_in_progress(hass) is False @@ -201,7 +200,7 @@ async def test_events_during_migration_are_queued(hass): async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) - await hass.data[DATA_INSTANCE].async_recorder_ready.wait() + await recorder.get_instance(hass).async_recorder_ready.wait() await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False @@ -232,7 +231,7 @@ async def test_events_during_migration_queue_exhausted(hass): async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) await hass.async_block_till_done() hass.states.async_set("my.entity", "off", {}) - await hass.data[DATA_INSTANCE].async_recorder_ready.wait() + await recorder.get_instance(hass).async_recorder_ready.wait() await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 30a2926844b..ee76b40a15b 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -12,7 +12,7 @@ from sqlalchemy.orm import Session from homeassistant.components import recorder from homeassistant.components.recorder import history, statistics -from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX +from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import StatisticsShortTerm from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( @@ -45,7 +45,7 @@ ORIG_TZ = dt_util.DEFAULT_TIME_ZONE def test_compile_hourly_statistics(hass_recorder): """Test compiling hourly statistics.""" hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] + instance = recorder.get_instance(hass) setup_component(hass, "sensor", {}) zero, four, states = record_states(hass) hist = history.get_significant_states(hass, zero, four) @@ -142,7 +142,7 @@ def test_compile_hourly_statistics(hass_recorder): stats = get_last_short_term_statistics(hass, 1, "sensor.test3", True) assert stats == {} - recorder.get_session().query(StatisticsShortTerm).delete() + instance.get_session().query(StatisticsShortTerm).delete() # Should not fail there is nothing in the table stats = get_latest_short_term_statistics(hass, ["sensor.test1"]) assert stats == {} diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 8624719f951..ac4eeada3d3 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -13,7 +13,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util -from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX +from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.models import UnsupportedDialect from homeassistant.components.recorder.util import ( @@ -35,7 +35,7 @@ def test_session_scope_not_setup(hass_recorder): """Try to create a session scope when not setup.""" hass = hass_recorder() with patch.object( - hass.data[DATA_INSTANCE], "get_session", return_value=None + util.get_instance(hass), "get_session", return_value=None ), pytest.raises(RuntimeError): with util.session_scope(hass=hass): pass @@ -547,7 +547,7 @@ def test_basic_sanity_check(hass_recorder): """Test the basic sanity checks with a missing table.""" hass = hass_recorder() - cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor() + cursor = util.get_instance(hass).engine.raw_connection().cursor() assert util.basic_sanity_check(cursor) is True @@ -560,7 +560,7 @@ def test_basic_sanity_check(hass_recorder): def test_combined_checks(hass_recorder, caplog): """Run Checks on the open database.""" hass = hass_recorder() - instance = recorder.get_instance(hass) + instance = util.get_instance(hass) instance.db_retry_wait = 0 cursor = instance.engine.raw_connection().cursor() @@ -639,8 +639,8 @@ def test_end_incomplete_runs(hass_recorder, caplog): def test_periodic_db_cleanups(hass_recorder): """Test periodic db cleanups.""" hass = hass_recorder() - with patch.object(hass.data[DATA_INSTANCE].engine, "connect") as connect_mock: - util.periodic_db_cleanups(hass.data[DATA_INSTANCE]) + with patch.object(util.get_instance(hass).engine, "connect") as connect_mock: + util.periodic_db_cleanups(util.get_instance(hass)) text_obj = connect_mock.return_value.__enter__.return_value.execute.mock_calls[0][ 1 @@ -663,11 +663,9 @@ async def test_write_lock_db( config = { recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db?timeout=0.1") } - await async_setup_recorder_instance(hass, config) + instance = await async_setup_recorder_instance(hass, config) await hass.async_block_till_done() - instance = hass.data[DATA_INSTANCE] - def _drop_table(): with instance.engine.connect() as connection: connection.execute(text("DROP TABLE events;")) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index a7ac01cf1d1..283883030fa 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -8,7 +8,6 @@ import pytest from pytest import approx from homeassistant.components import recorder -from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, @@ -304,7 +303,7 @@ async def test_recorder_info_bad_recorder_config(hass, hass_ws_client): await hass.async_block_till_done() # Wait for recorder to shut down - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_add_executor_job(recorder.get_instance(hass).join) await client.send_json({"id": 1, "type": "recorder/info"}) response = await client.receive_json() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 4be59e4c82c..cc2f9c76f1f 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -10,7 +10,6 @@ from pytest import approx from homeassistant import loader from homeassistant.components.recorder import history -from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.db_schema import StatisticsMeta from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( @@ -18,7 +17,7 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, statistics_during_period, ) -from homeassistant.components.recorder.util import session_scope +from homeassistant.components.recorder.util import get_instance, session_scope from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util @@ -2290,7 +2289,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): hass = hass_recorder() # Remove this after dropping the use of the hass_recorder fixture hass.config.set_time_zone("America/Regina") - recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -2454,7 +2453,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): sum_adjustement_start = zero + timedelta(minutes=65) for i in range(13, 24): expected_sums["sensor.test4"][i] += sum_adjustment - recorder.async_adjust_statistics( + instance.async_adjust_statistics( "sensor.test4", sum_adjustement_start, sum_adjustment ) wait_recording_done(hass) From 9d0a252ca73ea1b002bf13c7d77f652d5542794b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Jul 2022 13:36:43 +0200 Subject: [PATCH 628/820] Improve handling of MQTT config entry data (#72691) * Improve handling of MQTT config entry data * Add test * Add warning * Adjust tests --- homeassistant/components/mqtt/__init__.py | 31 ++++++ tests/components/mqtt/test_diagnostics.py | 2 +- tests/components/mqtt/test_discovery.py | 18 +++- tests/components/mqtt/test_init.py | 122 ++++++++++++---------- tests/conftest.py | 31 ++++-- 5 files changed, 135 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 394e4af3e2e..2bea1a593d1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -107,6 +107,16 @@ CONNECTION_SUCCESS = "connection_success" CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" +CONFIG_ENTRY_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -185,6 +195,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +def _filter_entry_config(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove unknown keys from config entry data. + + Extra keys may have been added when importing MQTT yaml configuration. + """ + filtered_data = { + k: entry.data[k] for k in CONFIG_ENTRY_CONFIG_KEYS if k in entry.data + } + if entry.data.keys() != filtered_data.keys(): + _LOGGER.warning( + "The following unsupported configuration options were removed from the " + "MQTT config entry: %s. Add them to configuration.yaml if they are needed", + entry.data.keys() - filtered_data.keys(), + ) + hass.config_entries.async_update_entry(entry, data=filtered_data) + + def _merge_basic_config( hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any] ) -> None: @@ -243,6 +270,10 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) hass.data[DATA_MQTT_CONFIG] = mqtt_config + # Remove unknown keys from config entry data + _filter_entry_config(hass, entry) + + # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) # Bail out if broker setting is missing if CONF_BROKER not in entry.data: diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index 8cc5d0b1070..7c486f9af74 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -158,7 +158,7 @@ async def test_entry_diagnostics( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index d185d3334d0..e4c6f44883a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -46,7 +46,7 @@ def entity_reg(hass): @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_subscribing_config_topic(hass, mqtt_mock_entry_no_yaml_config): @@ -1238,19 +1238,27 @@ async def test_no_implicit_state_topic_switch( @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register", } ], ) async def test_complex_discovery_topic_prefix( - hass, mqtt_mock_entry_no_yaml_config, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Tests handling of discovery topic prefix with multiple slashes.""" - await mqtt_mock_entry_no_yaml_config() + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register", + } + }, + ) + async_fire_mqtt_message( hass, ("my_home/homeassistant/register/binary_sensor/node1/object1/config"), diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index cec6038ae04..1f263d40fc2 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1249,7 +1249,7 @@ async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_ @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_restore_subscriptions_on_reconnect( @@ -1272,7 +1272,7 @@ async def test_restore_subscriptions_on_reconnect( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_restore_all_active_subscriptions_on_reconnect( @@ -1486,19 +1486,19 @@ async def test_setup_manual_mqtt_empty_platform(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", []) -async def test_setup_mqtt_client_protocol(hass): +async def test_setup_mqtt_client_protocol(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT client protocol setup.""" - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={ - mqtt.CONF_BROKER: "test-broker", - mqtt.config_integration.CONF_PROTOCOL: "3.1", - }, - ) - entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + mqtt.config_integration.CONF_PROTOCOL: "3.1", + } + }, + ) mock_client.on_connect(return_value=0) - assert await mqtt.async_setup_entry(hass, entry) await hass.async_block_till_done() # check if protocol setup was correctly @@ -1563,9 +1563,17 @@ async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplo assert "Failed to connect to MQTT server due to exception:" in caplog.text -@pytest.mark.parametrize("insecure", [None, False, True]) +@pytest.mark.parametrize( + "config, insecure_param", + [ + ({"certificate": "auto"}, "not set"), + ({"certificate": "auto", "tls_insecure": False}, False), + ({"certificate": "auto", "tls_insecure": True}, True), + ], +) +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( - hass, insecure + hass, config, insecure_param, mqtt_mock_entry_with_yaml_config ): """Test setup uses bundled certs when certificate is set to auto and insecure.""" calls = [] @@ -1577,18 +1585,14 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( def mock_tls_insecure_set(insecure_param): insecure_check["insecure"] = insecure_param - config_item_data = {mqtt.CONF_BROKER: "test-broker", "certificate": "auto"} - if insecure is not None: - config_item_data["tls_insecure"] = insecure with patch("paho.mqtt.client.Client") as mock_client: mock_client().tls_set = mock_tls_set mock_client().tls_insecure_set = mock_tls_insecure_set - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data=config_item_data, + assert await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: config}, ) - entry.add_to_hass(hass) - assert await mqtt.async_setup_entry(hass, entry) await hass.async_block_till_done() assert calls @@ -1596,19 +1600,14 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( import certifi expectedCertificate = certifi.where() - # assert mock_mqtt.mock_calls[0][1][2]["certificate"] == expectedCertificate assert calls[0][0] == expectedCertificate # test if insecure is set - assert ( - insecure_check["insecure"] == insecure - if insecure is not None - else insecure_check["insecure"] == "not set" - ) + assert insecure_check["insecure"] == insecure_param -async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): - """Test setup defaults to TLSv1 under python3.6.""" +async def test_tls_version(hass, mqtt_mock_entry_with_yaml_config): + """Test setup defaults for tls.""" calls = [] def mock_tls_set(certificate, certfile=None, keyfile=None, tls_version=None): @@ -1616,28 +1615,19 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): with patch("paho.mqtt.client.Client") as mock_client: mock_client().tls_set = mock_tls_set - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, + assert await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {"certificate": "auto"}}, ) - entry.add_to_hass(hass) - assert await mqtt.async_setup_entry(hass, entry) await hass.async_block_till_done() assert calls - - import sys - - if sys.hexversion >= 0x03060000: - expectedTlsVersion = ssl.PROTOCOL_TLS # pylint: disable=no-member - else: - expectedTlsVersion = ssl.PROTOCOL_TLSv1 - - assert calls[0][3] == expectedTlsVersion + assert calls[0][3] == ssl.PROTOCOL_TLS @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1670,7 +1660,7 @@ async def test_custom_birth_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1705,7 +1695,7 @@ async def test_default_birth_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], ) async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): @@ -1719,7 +1709,7 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_ @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1733,7 +1723,7 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_ ], ) async def test_delayed_birth_message( - hass, mqtt_client_mock, mqtt_config, mqtt_mock_entry_no_yaml_config + hass, mqtt_client_mock, mqtt_config_entry_data, mqtt_mock_entry_no_yaml_config ): """Test sending birth message does not happen until Home Assistant starts.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() @@ -1743,7 +1733,7 @@ async def test_delayed_birth_message( await hass.async_block_till_done() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config) + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1780,7 +1770,7 @@ async def test_delayed_birth_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1816,7 +1806,7 @@ async def test_default_will_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], ) async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): @@ -1827,7 +1817,7 @@ async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_c @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -2922,3 +2912,29 @@ async def test_setup_manual_items_with_unique_ids( assert hass.states.get("light.test1") is not None assert (hass.states.get("light.test2") is not None) == unique assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique + + +async def test_remove_unknown_conf_entry_options(hass, mqtt_client_mock, caplog): + """Test unknown keys in config entry data is removed.""" + mqtt_config_entry_data = { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: {}, + mqtt.client.CONF_PROTOCOL: mqtt.const.PROTOCOL_311, + } + + entry = MockConfigEntry( + data=mqtt_config_entry_data, + domain=mqtt.DOMAIN, + title="MQTT", + ) + + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mqtt.client.CONF_PROTOCOL not in entry.data + assert ( + "The following unsupported configuration options were removed from the " + "MQTT config entry: {'protocol'}. Add them to configuration.yaml if they " + "are needed" + ) in caplog.text diff --git a/tests/conftest.py b/tests/conftest.py index ee2e903e88d..dc5f3069332 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -501,7 +501,7 @@ def fail_on_log_exception(request, monkeypatch): @pytest.fixture -def mqtt_config(): +def mqtt_config_entry_data(): """Fixture to allow overriding MQTT config.""" return None @@ -553,7 +553,7 @@ def mqtt_client_mock(hass): async def mqtt_mock( hass, mqtt_client_mock, - mqtt_config, + mqtt_config_entry_data, mqtt_mock_entry_no_yaml_config, ): """Fixture to mock MQTT component.""" @@ -561,15 +561,18 @@ async def mqtt_mock( @asynccontextmanager -async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config): +async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config_entry_data): """Fixture to mock a delayed setup of the MQTT config entry.""" - if mqtt_config is None: - mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}} + if mqtt_config_entry_data is None: + mqtt_config_entry_data = { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: {}, + } await hass.async_block_till_done() entry = MockConfigEntry( - data=mqtt_config, + data=mqtt_config_entry_data, domain=mqtt.DOMAIN, title="MQTT", ) @@ -613,7 +616,9 @@ async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config): @pytest.fixture -async def mqtt_mock_entry_no_yaml_config(hass, mqtt_client_mock, mqtt_config): +async def mqtt_mock_entry_no_yaml_config( + hass, mqtt_client_mock, mqtt_config_entry_data +): """Set up an MQTT config entry without MQTT yaml config.""" async def _async_setup_config_entry(hass, entry): @@ -626,12 +631,16 @@ async def mqtt_mock_entry_no_yaml_config(hass, mqtt_client_mock, mqtt_config): """Set up the MQTT config entry.""" return await mqtt_mock_entry(_async_setup_config_entry) - async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + async with _mqtt_mock_entry( + hass, mqtt_client_mock, mqtt_config_entry_data + ) as mqtt_mock_entry: yield _setup_mqtt_entry @pytest.fixture -async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): +async def mqtt_mock_entry_with_yaml_config( + hass, mqtt_client_mock, mqtt_config_entry_data +): """Set up an MQTT config entry with MQTT yaml config.""" async def _async_do_not_setup_config_entry(hass, entry): @@ -642,7 +651,9 @@ async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): """Set up the MQTT config entry.""" return await mqtt_mock_entry(_async_do_not_setup_config_entry) - async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + async with _mqtt_mock_entry( + hass, mqtt_client_mock, mqtt_config_entry_data + ) as mqtt_mock_entry: yield _setup_mqtt_entry From fd6ffef52f337df71542b48565a95300c0ab2766 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Jul 2022 15:11:34 +0200 Subject: [PATCH 629/820] Support non-live database migration (#72433) * Support non-live database migration * Tweak startup order, add test * Address review comments * Fix typo * Clarify comment about promoting dependencies * Tweak * Fix merge mistake * Fix some tests * Fix additional test * Fix additional test * Adjust tests * Improve test coverage --- homeassistant/bootstrap.py | 75 +- homeassistant/components/recorder/__init__.py | 1 - homeassistant/components/recorder/core.py | 42 +- .../components/recorder/migration.py | 15 +- homeassistant/components/recorder/models.py | 10 + .../components/recorder/statistics.py | 6 +- homeassistant/components/recorder/tasks.py | 2 +- homeassistant/components/recorder/util.py | 14 +- .../components/recorder/websocket_api.py | 4 +- homeassistant/helpers/recorder.py | 26 +- tests/common.py | 3 + tests/components/default_config/test_init.py | 2 + tests/components/recorder/db_schema_25.py | 673 ++++++++++++++++++ tests/components/recorder/test_init.py | 25 +- tests/components/recorder/test_migrate.py | 47 +- tests/components/recorder/test_statistics.py | 5 + .../recorder/test_statistics_v23_migration.py | 9 + .../components/recorder/test_websocket_api.py | 15 +- tests/conftest.py | 4 +- tests/test_bootstrap.py | 76 ++ 20 files changed, 993 insertions(+), 61 deletions(-) create mode 100644 tests/components/recorder/db_schema_25.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b8ec5987142..d2858bfcdf1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,7 +24,7 @@ from .const import ( SIGNAL_BOOTSTRAP_INTEGRATONS, ) from .exceptions import HomeAssistantError -from .helpers import area_registry, device_registry, entity_registry +from .helpers import area_registry, device_registry, entity_registry, recorder from .helpers.dispatcher import async_dispatcher_send from .helpers.typing import ConfigType from .setup import ( @@ -66,6 +66,15 @@ LOGGING_INTEGRATIONS = { # Error logging "system_log", "sentry", +} +FRONTEND_INTEGRATIONS = { + # Get the frontend up and running as soon as possible so problem + # integrations can be removed and database migration status is + # visible in frontend + "frontend", +} +RECORDER_INTEGRATIONS = { + # Setup after frontend # To record data "recorder", } @@ -83,10 +92,6 @@ STAGE_1_INTEGRATIONS = { "cloud", # Ensure supervisor is available "hassio", - # Get the frontend up and running as soon - # as possible so problem integrations can - # be removed - "frontend", } @@ -504,11 +509,43 @@ async def _async_set_up_integrations( _LOGGER.info("Domains to be set up: %s", domains_to_setup) + def _cache_uname_processor() -> None: + """Cache the result of platform.uname().processor in the executor. + + Multiple modules call this function at startup which + executes a blocking subprocess call. This is a problem for the + asyncio event loop. By primeing the cache of uname we can + avoid the blocking call in the event loop. + """ + platform.uname().processor # pylint: disable=expression-not-assigned + + # Load the registries and cache the result of platform.uname().processor + await asyncio.gather( + device_registry.async_load(hass), + entity_registry.async_load(hass), + area_registry.async_load(hass), + hass.async_add_executor_job(_cache_uname_processor), + ) + + # Initialize recorder + if "recorder" in domains_to_setup: + recorder.async_initialize_recorder(hass) + # Load logging as soon as possible if logging_domains := domains_to_setup & LOGGING_INTEGRATIONS: _LOGGER.info("Setting up logging: %s", logging_domains) await async_setup_multi_components(hass, logging_domains, config) + # Setup frontend + if frontend_domains := domains_to_setup & FRONTEND_INTEGRATIONS: + _LOGGER.info("Setting up frontend: %s", frontend_domains) + await async_setup_multi_components(hass, frontend_domains, config) + + # Setup recorder + if recorder_domains := domains_to_setup & RECORDER_INTEGRATIONS: + _LOGGER.info("Setting up recorder: %s", recorder_domains) + await async_setup_multi_components(hass, recorder_domains, config) + # Start up debuggers. Start these first in case they want to wait. if debuggers := domains_to_setup & DEBUGGER_INTEGRATIONS: _LOGGER.debug("Setting up debuggers: %s", debuggers) @@ -518,7 +555,8 @@ async def _async_set_up_integrations( stage_1_domains: set[str] = set() # Find all dependencies of any dependency of any stage 1 integration that - # we plan on loading and promote them to stage 1 + # we plan on loading and promote them to stage 1. This is done only to not + # get misleading log messages deps_promotion: set[str] = STAGE_1_INTEGRATIONS while deps_promotion: old_deps_promotion = deps_promotion @@ -535,24 +573,13 @@ async def _async_set_up_integrations( deps_promotion.update(dep_itg.all_dependencies) - stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains - - def _cache_uname_processor() -> None: - """Cache the result of platform.uname().processor in the executor. - - Multiple modules call this function at startup which - executes a blocking subprocess call. This is a problem for the - asyncio event loop. By primeing the cache of uname we can - avoid the blocking call in the event loop. - """ - platform.uname().processor # pylint: disable=expression-not-assigned - - # Load the registries - await asyncio.gather( - device_registry.async_load(hass), - entity_registry.async_load(hass), - area_registry.async_load(hass), - hass.async_add_executor_job(_cache_uname_processor), + stage_2_domains = ( + domains_to_setup + - logging_domains + - frontend_domains + - recorder_domains + - debuggers + - stage_1_domains ) # Start setup diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 238b013f366..f9ed5f59333 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -123,7 +123,6 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" - hass.data[DOMAIN] = {} exclude_attributes_by_domain: dict[str, set[str]] = {} hass.data[EXCLUDE_ATTRIBUTES] = exclude_attributes_by_domain conf = config[DOMAIN] diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index f3ae79f9909..92e10b47126 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -43,6 +43,7 @@ import homeassistant.util.dt as dt_util from . import migration, statistics from .const import ( DB_WORKER_PREFIX, + DOMAIN, KEEPALIVE_TIME, MAX_QUEUE_BACKLOG, MYSQLDB_URL_PREFIX, @@ -166,7 +167,12 @@ class Recorder(threading.Thread): self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait self.engine_version: AwesomeVersion | None = None + # Database connection is ready, but non-live migration may be in progress + db_connected: asyncio.Future[bool] = hass.data[DOMAIN].db_connected + self.async_db_connected: asyncio.Future[bool] = db_connected + # Database is ready to use but live migration may be in progress self.async_db_ready: asyncio.Future[bool] = asyncio.Future() + # Database is ready to use and all migration steps completed (used by tests) self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() self.engine: Engine | None = None @@ -188,6 +194,7 @@ class Recorder(threading.Thread): self._completed_first_database_setup: bool | None = None self.async_migration_event = asyncio.Event() self.migration_in_progress = False + self.migration_is_live = False self._database_lock_task: DatabaseLockTask | None = None self._db_executor: DBInterruptibleThreadPoolExecutor | None = None self._exclude_attributes_by_domain = exclude_attributes_by_domain @@ -289,7 +296,8 @@ class Recorder(threading.Thread): def _stop_executor(self) -> None: """Stop the executor.""" - assert self._db_executor is not None + if self._db_executor is None: + return self._db_executor.shutdown() self._db_executor = None @@ -410,6 +418,7 @@ class Recorder(threading.Thread): @callback def async_connection_failed(self) -> None: """Connect failed tasks.""" + self.async_db_connected.set_result(False) self.async_db_ready.set_result(False) persistent_notification.async_create( self.hass, @@ -420,13 +429,29 @@ class Recorder(threading.Thread): @callback def async_connection_success(self) -> None: - """Connect success tasks.""" + """Connect to the database succeeded, schema version and migration need known. + + The database may not yet be ready for use in case of a non-live migration. + """ + self.async_db_connected.set_result(True) + + @callback + def async_set_recorder_ready(self) -> None: + """Database live and ready for use. + + Called after non-live migration steps are finished. + """ + if self.async_db_ready.done(): + return self.async_db_ready.set_result(True) self.async_start_executor() @callback - def _async_recorder_ready(self) -> None: - """Finish start and mark recorder ready.""" + def _async_set_recorder_ready_migration_done(self) -> None: + """Finish start and mark recorder ready. + + Called after all migration steps are finished. + """ self._async_setup_periodic_tasks() self.async_recorder_ready.set() @@ -548,6 +573,7 @@ class Recorder(threading.Thread): self._setup_run() else: self.migration_in_progress = True + self.migration_is_live = migration.live_migration(current_version) self.hass.add_job(self.async_connection_success) @@ -557,6 +583,7 @@ class Recorder(threading.Thread): # Make sure we cleanly close the run if # we restart before startup finishes self._shutdown() + self.hass.add_job(self.async_set_recorder_ready) return # We wait to start the migration until startup has finished @@ -577,11 +604,14 @@ class Recorder(threading.Thread): "Database Migration Failed", "recorder_database_migration", ) + self.hass.add_job(self.async_set_recorder_ready) self._shutdown() return + self.hass.add_job(self.async_set_recorder_ready) + _LOGGER.debug("Recorder processing the queue") - self.hass.add_job(self._async_recorder_ready) + self.hass.add_job(self._async_set_recorder_ready_migration_done) self._run_event_loop() def _run_event_loop(self) -> None: @@ -659,7 +689,7 @@ class Recorder(threading.Thread): try: migration.migrate_schema( - self.hass, self.engine, self.get_session, current_version + self, self.hass, self.engine, self.get_session, current_version ) except exc.DatabaseError as err: if self._handle_database_error(err): diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 7e11e62502d..de6fd8f01fe 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Iterable import contextlib from datetime import timedelta import logging -from typing import cast +from typing import Any, cast import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text @@ -40,6 +40,8 @@ from .statistics import ( ) from .util import session_scope +LIVE_MIGRATION_MIN_SCHEMA_VERSION = 0 + _LOGGER = logging.getLogger(__name__) @@ -78,7 +80,13 @@ def schema_is_current(current_version: int) -> bool: return current_version == SCHEMA_VERSION +def live_migration(current_version: int) -> bool: + """Check if live migration is possible.""" + return current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION + + def migrate_schema( + instance: Any, hass: HomeAssistant, engine: Engine, session_maker: Callable[[], Session], @@ -86,7 +94,12 @@ def migrate_schema( ) -> None: """Check if the schema needs to be upgraded.""" _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) + db_ready = False for version in range(current_version, SCHEMA_VERSION): + if live_migration(version) and not db_ready: + db_ready = True + instance.migration_is_live = True + hass.add_job(instance.async_set_recorder_ready) new_version = version + 1 _LOGGER.info("Upgrading recorder db schema to version %s", new_version) _apply_update(hass, engine, session_maker, new_version, current_version) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ff53d9be3d1..98c9fc7c9b2 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,6 +1,8 @@ """Models for Recorder.""" from __future__ import annotations +import asyncio +from dataclasses import dataclass, field from datetime import datetime import logging from typing import Any, TypedDict, overload @@ -30,6 +32,14 @@ class UnsupportedDialect(Exception): """The dialect or its version is not supported.""" +@dataclass +class RecorderData: + """Recorder data stored in hass.data.""" + + recorder_platforms: dict[str, Any] = field(default_factory=dict) + db_connected: asyncio.Future = field(default_factory=asyncio.Future) + + class StatisticResult(TypedDict): """Statistic result data class. diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index bce77e8a31e..fcd8e4f3930 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -576,7 +576,7 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: platform_stats: list[StatisticResult] = [] current_metadata: dict[str, tuple[int, StatisticMetaData]] = {} # Collect statistics from all platforms implementing support - for domain, platform in instance.hass.data[DOMAIN].items(): + for domain, platform in instance.hass.data[DOMAIN].recorder_platforms.items(): if not hasattr(platform, "compile_statistics"): continue compiled: PlatformCompiledStatistics = platform.compile_statistics( @@ -851,7 +851,7 @@ def list_statistic_ids( } # Query all integrations with a registered recorder platform - for platform in hass.data[DOMAIN].values(): + for platform in hass.data[DOMAIN].recorder_platforms.values(): if not hasattr(platform, "list_statistic_ids"): continue platform_statistic_ids = platform.list_statistic_ids( @@ -1339,7 +1339,7 @@ def _sorted_statistics_to_dict( def validate_statistics(hass: HomeAssistant) -> dict[str, list[ValidationIssue]]: """Validate statistics.""" platform_validation: dict[str, list[ValidationIssue]] = {} - for platform in hass.data[DOMAIN].values(): + for platform in hass.data[DOMAIN].recorder_platforms.values(): if not hasattr(platform, "validate_statistics"): continue platform_validation.update(platform.validate_statistics(hass)) diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 6d1c9c360ab..cdb97d9d67c 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -249,7 +249,7 @@ class AddRecorderPlatformTask(RecorderTask): domain = self.domain platform = self.platform - platforms: dict[str, Any] = hass.data[DOMAIN] + platforms: dict[str, Any] = hass.data[DOMAIN].recorder_platforms platforms[domain] = platform if hasattr(self.platform, "exclude_attributes"): hass.data[EXCLUDE_ATTRIBUTES][domain] = platform.exclude_attributes(hass) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index fdf42665ef5..ddc8747f79b 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -552,7 +552,7 @@ def write_lock_db_sqlite(instance: Recorder) -> Generator[None, None, None]: def async_migration_in_progress(hass: HomeAssistant) -> bool: - """Determine is a migration is in progress. + """Determine if a migration is in progress. This is a thin wrapper that allows us to change out the implementation later. @@ -563,6 +563,18 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: return instance.migration_in_progress +def async_migration_is_live(hass: HomeAssistant) -> bool: + """Determine if a migration is live. + + This is a thin wrapper that allows us to change + out the implementation later. + """ + if DATA_INSTANCE not in hass.data: + return False + instance: Recorder = hass.data[DATA_INSTANCE] + return instance.migration_is_live + + def second_sunday(year: int, month: int) -> date: """Return the datetime.date for the second sunday of a month.""" second = date(year, month, FIRST_POSSIBLE_SUNDAY) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index c143d8b4f0b..16813944780 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -17,7 +17,7 @@ from .statistics import ( list_statistic_ids, validate_statistics, ) -from .util import async_migration_in_progress, get_instance +from .util import async_migration_in_progress, async_migration_is_live, get_instance _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -193,6 +193,7 @@ def ws_info( backlog = instance.backlog if instance else None migration_in_progress = async_migration_in_progress(hass) + migration_is_live = async_migration_is_live(hass) recording = instance.recording if instance else False thread_alive = instance.is_alive() if instance else False @@ -200,6 +201,7 @@ def ws_info( "backlog": backlog, "max_backlog": MAX_QUEUE_BACKLOG, "migration_in_progress": migration_in_progress, + "migration_is_live": migration_is_live, "recording": recording, "thread_running": thread_alive, } diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index a51d9de59e2..2049300e460 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -1,7 +1,8 @@ """Helpers to check recorder.""" +import asyncio -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback def async_migration_in_progress(hass: HomeAssistant) -> bool: @@ -12,3 +13,26 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: from homeassistant.components import recorder return recorder.util.async_migration_in_progress(hass) + + +@callback +def async_initialize_recorder(hass: HomeAssistant) -> None: + """Initialize recorder data.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.recorder import const, models + + hass.data[const.DOMAIN] = models.RecorderData() + + +async def async_wait_recorder(hass: HomeAssistant) -> bool: + """Wait for recorder to initialize and return connection status. + + Returns False immediately if the recorder is not enabled. + """ + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.recorder import const + + if const.DOMAIN not in hass.data: + return False + db_connected: asyncio.Future[bool] = hass.data[const.DOMAIN].db_connected + return await db_connected diff --git a/tests/common.py b/tests/common.py index 80f0913cace..acc50e26889 100644 --- a/tests/common.py +++ b/tests/common.py @@ -50,6 +50,7 @@ from homeassistant.helpers import ( entity_platform, entity_registry, intent, + recorder as recorder_helper, restore_state, storage, ) @@ -914,6 +915,8 @@ def init_recorder_component(hass, add_config=None): with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration.migrate_schema" ): + if recorder.DOMAIN not in hass.data: + recorder_helper.async_initialize_recorder(hass) assert setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) assert recorder.DOMAIN in hass.config.components _LOGGER.info( diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 7701eb55b90..d82b4109839 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @@ -24,4 +25,5 @@ def recorder_url_mock(): async def test_setup(hass, mock_zeroconf, mock_get_source_ip): """Test setup.""" + recorder_helper.async_initialize_recorder(hass) assert await async_setup_component(hass, "default_config", {"foo": "bar"}) diff --git a/tests/components/recorder/db_schema_25.py b/tests/components/recorder/db_schema_25.py new file mode 100644 index 00000000000..43aa245a761 --- /dev/null +++ b/tests/components/recorder/db_schema_25.py @@ -0,0 +1,673 @@ +"""Models for SQLAlchemy.""" +from __future__ import annotations + +from datetime import datetime, timedelta +import json +import logging +from typing import Any, TypedDict, cast, overload + +from fnvhash import fnv1a_32 +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + String, + Text, + distinct, +) +from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.engine.row import Row +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.components.recorder.const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.typing import UNDEFINED, UndefinedType +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 25 + +_LOGGER = logging.getLogger(__name__) + +DB_TIMEZONE = "+00:00" + +TABLE_EVENTS = "events" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +EMPTY_JSON_OBJECT = "{}" + + +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) + + +class Events(Base): # type: ignore[misc,valid-type] + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) + time_fired = Column(DATETIME_TYPE, index=True) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event( + event: Event, event_data: UndefinedType | None = UNDEFINED + ) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=JSON_DUMP(event.data) if event_data is UNDEFINED else event_data, + origin=str(event.origin.value), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data), + EventOrigin(self.origin), + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class States(Base): # type: ignore[misc,valid-type] + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + attributes_id = Column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + event = relationship("Events", uselist=False) + old_state = relationship("States", remote_side=[state_id]) + state_attributes = relationship("StateAttributes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> States: + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state: State | None = event.data.get("new_state") + dbstate = States(entity_id=entity_id, attributes=None) + + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.state = state.state + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + try: + return State( + self.entity_id, + self.state, + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + json.loads(self.attributes) if self.attributes else {}, + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), + # Join the events table on event_id to get the context instead + # as it will always be there for state_changed events + context=Context(id=None), # type: ignore[arg-type] + validate_entity_id=validate_entity_id, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class StateAttributes(Base): # type: ignore[misc,valid-type] + """State attribute change history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> StateAttributes: + """Create object from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + dbstate = StateAttributes( + shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) + ) + dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) + return dbstate + + @staticmethod + def shared_attrs_from_event( + event: Event, exclude_attrs_by_domain: dict[str, set[str]] + ) -> str: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return "{}" + domain = split_entity_id(state.entity_id)[0] + exclude_attrs = ( + exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS + ) + return JSON_DUMP( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + + @staticmethod + def hash_shared_attrs(shared_attrs: str) -> int: + """Return the hash of json encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json.loads(self.shared_attrs)) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatisticResult(TypedDict): + """Statistic result data class. + + Allows multiple datapoints for the same statistic_id. + """ + + meta: StatisticMetaData + stat: StatisticData + + +class StatisticDataBase(TypedDict): + """Mandatory fields for statistic data class.""" + + start: datetime + + +class StatisticData(StatisticDataBase, total=False): + """Statistic data class.""" + + mean: float + min: float + max: float + last_reset: datetime | None + state: float + sum: float + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr # type: ignore[misc] + def metadata_id(self) -> Column: + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: + """Create object from a statistics.""" + return cls( # type: ignore[call-arg,misc] + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start", + "metadata_id", + "start", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticMetaData(TypedDict): + """Statistic meta data class.""" + + has_mean: bool + has_sum: bool + name: str | None + source: str + statistic_id: str + unit_of_measurement: str | None + + +class StatisticsMeta(Base): # type: ignore[misc,valid-type] + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + name = Column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): # type: ignore[misc,valid-type] + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore[misc,valid-type] + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +class StatisticsRuns(Base): # type: ignore[misc,valid-type] + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True)) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +@overload +def process_timestamp(ts: None) -> None: + ... + + +@overload +def process_timestamp(ts: datetime) -> datetime: + ... + + +def process_timestamp(ts: datetime | None) -> datetime | None: + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) + + +@overload +def process_timestamp_to_utc_isoformat(ts: None) -> None: + ... + + +@overload +def process_timestamp_to_utc_isoformat(ts: datetime) -> str: + ... + + +def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: + """Process a timestamp into UTC isotime.""" + if ts is None: + return None + if ts.tzinfo == dt_util.UTC: + return ts.isoformat() + if ts.tzinfo is None: + return f"{ts.isoformat()}{DB_TIMEZONE}" + return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + "_attr_cache", + ] + + def __init__( # pylint: disable=super-init-not-called + self, row: Row, attr_cache: dict[str, dict[str, Any]] | None = None + ) -> None: + """Init the lazy state.""" + self._row = row + self.entity_id: str = self._row.entity_id + self.state = self._row.state or "" + self._attributes: dict[str, Any] | None = None + self._last_changed: datetime | None = None + self._last_updated: datetime | None = None + self._context: Context | None = None + self._attr_cache = attr_cache + + @property # type: ignore[override] + def attributes(self) -> dict[str, Any]: # type: ignore[override] + """State attributes.""" + if self._attributes is None: + source = self._row.shared_attrs or self._row.attributes + if self._attr_cache is not None and ( + attributes := self._attr_cache.get(source) + ): + self._attributes = attributes + return attributes + if source == EMPTY_JSON_OBJECT or source is None: + self._attributes = {} + return self._attributes + try: + self._attributes = json.loads(source) + except ValueError: + # When json.loads fails + _LOGGER.exception( + "Error converting row to state attributes: %s", self._row + ) + self._attributes = {} + if self._attr_cache is not None: + self._attr_cache[source] = self._attributes + return self._attributes + + @attributes.setter + def attributes(self, value: dict[str, Any]) -> None: + """Set attributes.""" + self._attributes = value + + @property # type: ignore[override] + def context(self) -> Context: # type: ignore[override] + """State context.""" + if self._context is None: + self._context = Context(id=None) # type: ignore[arg-type] + return self._context + + @context.setter + def context(self, value: Context) -> None: + """Set context.""" + self._context = value + + @property # type: ignore[override] + def last_changed(self) -> datetime: # type: ignore[override] + """Last changed datetime.""" + if self._last_changed is None: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value: datetime) -> None: + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore[override] + def last_updated(self) -> datetime: # type: ignore[override] + """Last updated datetime.""" + if self._last_updated is None: + if (last_updated := self._row.last_updated) is not None: + self._last_updated = process_timestamp(last_updated) + else: + self._last_updated = self.last_changed + return self._last_updated + + @last_updated.setter + def last_updated(self, value: datetime) -> None: + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self) -> dict[str, Any]: # type: ignore[override] + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed is None and self._last_updated is None: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if ( + self._row.last_updated is None + or self._row.last_changed == self._row.last_updated + ): + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + else: + last_changed_isoformat = self.last_changed.isoformat() + if self.last_changed == self.last_updated: + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = self.last_updated.isoformat() + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other: Any) -> bool: + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 82444f86a05..0c3a41ab8ef 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -51,6 +51,7 @@ from homeassistant.const import ( STATE_UNLOCKED, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util @@ -100,9 +101,10 @@ async def test_shutdown_before_startup_finishes( } hass.state = CoreState.not_running - instance = await async_setup_recorder_instance(hass, config) - await instance.async_db_ready - await hass.async_block_till_done() + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass, config)) + await recorder_helper.async_wait_recorder(hass) + instance = get_instance(hass) session = await hass.async_add_executor_job(instance.get_session) @@ -125,9 +127,11 @@ async def test_canceled_before_startup_finishes( ): """Test recorder shuts down when its startup future is canceled out from under it.""" hass.state = CoreState.not_running - await async_setup_recorder_instance(hass) + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass)) + await recorder_helper.async_wait_recorder(hass) + instance = get_instance(hass) - await instance.async_db_ready instance._hass_started.cancel() with patch.object(instance, "engine"): await hass.async_block_till_done() @@ -170,7 +174,9 @@ async def test_state_gets_saved_when_set_before_start_event( hass.state = CoreState.not_running - await async_setup_recorder_instance(hass) + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass)) + await recorder_helper.async_wait_recorder(hass) entity_id = "test.recorder" state = "restoring_from_db" @@ -643,6 +649,7 @@ def test_saving_state_and_removing_entity(hass, hass_recorder): def test_recorder_setup_failure(hass): """Test some exceptions.""" + recorder_helper.async_initialize_recorder(hass) with patch.object(Recorder, "_setup_connection") as setup, patch( "homeassistant.components.recorder.core.time.sleep" ): @@ -657,6 +664,7 @@ def test_recorder_setup_failure(hass): def test_recorder_setup_failure_without_event_listener(hass): """Test recorder setup failure when the event listener is not setup.""" + recorder_helper.async_initialize_recorder(hass) with patch.object(Recorder, "_setup_connection") as setup, patch( "homeassistant.components.recorder.core.time.sleep" ): @@ -985,6 +993,7 @@ def test_compile_missing_statistics(tmpdir): ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1006,6 +1015,7 @@ def test_compile_missing_statistics(tmpdir): ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1197,6 +1207,7 @@ def test_service_disable_run_information_recorded(tmpdir): dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1218,6 +1229,7 @@ def test_service_disable_run_information_recorded(tmpdir): hass.stop() hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1246,6 +1258,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): test_db_file = await hass.async_add_executor_job(_create_tmpdir_for_test_db) dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl, CONF_COMMIT_INTERVAL: 0}} ) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index a57bd246f8b..bbac01bb5d3 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -27,6 +27,7 @@ from homeassistant.components.recorder.db_schema import ( States, ) from homeassistant.components.recorder.util import session_scope +from homeassistant.helpers import recorder as recorder_helper import homeassistant.util.dt as dt_util from .common import async_wait_recording_done, create_engine_test @@ -53,6 +54,7 @@ async def test_schema_update_calls(hass): "homeassistant.components.recorder.migration._apply_update", wraps=migration._apply_update, ) as update: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -74,10 +76,11 @@ async def test_migration_in_progress(hass): """Test that we can check for migration in progress.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True,), patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ): + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -105,6 +108,7 @@ async def test_database_migration_failed(hass): "homeassistant.components.persistent_notification.dismiss", side_effect=pn.dismiss, ) as mock_dismiss: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -136,6 +140,7 @@ async def test_database_migration_encounters_corruption(hass): ), patch( "homeassistant.components.recorder.core.move_away_broken_database" ) as move_away: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -165,6 +170,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): "homeassistant.components.persistent_notification.dismiss", side_effect=pn.dismiss, ) as mock_dismiss: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -189,6 +195,7 @@ async def test_events_during_migration_are_queued(hass): "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ): + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", @@ -219,6 +226,7 @@ async def test_events_during_migration_queue_exhausted(hass): "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ), patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1): + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", @@ -247,8 +255,11 @@ async def test_events_during_migration_queue_exhausted(hass): assert len(db_states) == 2 -@pytest.mark.parametrize("start_version", [0, 16, 18, 22]) -async def test_schema_migrate(hass, start_version): +@pytest.mark.parametrize( + "start_version,live", + [(0, True), (16, True), (18, True), (22, True), (25, True)], +) +async def test_schema_migrate(hass, start_version, live): """Test the full schema migration logic. We're just testing that the logic can execute successfully here without @@ -259,7 +270,8 @@ async def test_schema_migrate(hass, start_version): migration_done = threading.Event() migration_stall = threading.Event() migration_version = None - real_migration = recorder.migration.migrate_schema + real_migrate_schema = recorder.migration.migrate_schema + real_apply_update = recorder.migration._apply_update def _create_engine_test(*args, **kwargs): """Test version of create_engine that initializes with old schema. @@ -284,14 +296,12 @@ async def test_schema_migrate(hass, start_version): start=self.run_history.recording_start, created=dt_util.utcnow() ) - def _instrument_migration(*args): + def _instrument_migrate_schema(*args): """Control migration progress and check results.""" nonlocal migration_done nonlocal migration_version - nonlocal migration_stall - migration_stall.wait() try: - real_migration(*args) + real_migrate_schema(*args) except Exception: migration_done.set() raise @@ -307,6 +317,12 @@ async def test_schema_migrate(hass, start_version): migration_version = res.schema_version migration_done.set() + def _instrument_apply_update(*args): + """Control migration progress.""" + nonlocal migration_stall + migration_stall.wait() + real_apply_update(*args) + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=_create_engine_test, @@ -316,12 +332,21 @@ async def test_schema_migrate(hass, start_version): autospec=True, ) as setup_run, patch( "homeassistant.components.recorder.migration.migrate_schema", - wraps=_instrument_migration, + wraps=_instrument_migrate_schema, + ), patch( + "homeassistant.components.recorder.migration._apply_update", + wraps=_instrument_apply_update, ): - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + recorder_helper.async_initialize_recorder(hass) + hass.async_create_task( + async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) ) + await recorder_helper.async_wait_recorder(hass) + assert recorder.util.async_migration_in_progress(hass) is True + assert recorder.util.async_migration_is_live(hass) == live migration_stall.set() await hass.async_block_till_done() await hass.async_add_executor_job(migration_done.wait) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index ee76b40a15b..8db4587f1cf 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -31,6 +31,7 @@ from homeassistant.components.recorder.util import session_scope from homeassistant.const import TEMP_CELSIUS from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -1128,6 +1129,7 @@ def test_delete_metadata_duplicates(caplog, tmpdir): "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -1158,6 +1160,7 @@ def test_delete_metadata_duplicates(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 28 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -1217,6 +1220,7 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -1249,6 +1253,7 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 28 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index 50311a987d6..a7cc2b35e61 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -16,6 +16,7 @@ from sqlalchemy.orm import Session from homeassistant.components import recorder from homeassistant.components.recorder import SQLITE_URL_PREFIX, statistics from homeassistant.components.recorder.util import session_scope +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -179,6 +180,7 @@ def test_delete_duplicates(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -206,6 +208,7 @@ def test_delete_duplicates(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -347,6 +350,7 @@ def test_delete_duplicates_many(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -380,6 +384,7 @@ def test_delete_duplicates_many(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -492,6 +497,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -515,6 +521,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() hass.config.config_dir = tmpdir + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -592,6 +599,7 @@ def test_delete_duplicates_short_term(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -614,6 +622,7 @@ def test_delete_duplicates_short_term(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() hass.config.config_dir = tmpdir + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 283883030fa..b604cc53e6c 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -15,6 +15,7 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, statistics_during_period, ) +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM @@ -274,6 +275,7 @@ async def test_recorder_info(hass, hass_ws_client, recorder_mock): "backlog": 0, "max_backlog": 40000, "migration_in_progress": False, + "migration_is_live": False, "recording": True, "thread_running": True, } @@ -296,6 +298,7 @@ async def test_recorder_info_bad_recorder_config(hass, hass_ws_client): client = await hass_ws_client() with patch("homeassistant.components.recorder.migration.migrate_schema"): + recorder_helper.async_initialize_recorder(hass) assert not await async_setup_component( hass, recorder.DOMAIN, {recorder.DOMAIN: config} ) @@ -318,7 +321,7 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): migration_done = threading.Event() - real_migration = recorder.migration.migrate_schema + real_migration = recorder.migration._apply_update def stalled_migration(*args): """Make migration stall.""" @@ -334,12 +337,16 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): ), patch.object( recorder.core, "MAX_QUEUE_BACKLOG", 1 ), patch( - "homeassistant.components.recorder.migration.migrate_schema", + "homeassistant.components.recorder.migration._apply_update", wraps=stalled_migration, ): - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + recorder_helper.async_initialize_recorder(hass) + hass.create_task( + async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) ) + await recorder_helper.async_wait_recorder(hass) hass.states.async_set("my.entity", "on", {}) await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index dc5f3069332..9ca29c60658 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,7 +31,7 @@ from homeassistant.components.websocket_api.auth import ( from homeassistant.components.websocket_api.http import URL from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers import config_entry_oauth2_flow, recorder as recorder_helper from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, location @@ -790,6 +790,8 @@ async def _async_init_recorder_component(hass, add_config=None): with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration.migrate_schema" ): + if recorder.DOMAIN not in hass.data: + recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( hass, recorder.DOMAIN, {recorder.DOMAIN: config} ) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 232d8fb6bbf..06f800af7f3 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -211,6 +211,82 @@ async def test_setup_after_deps_in_stage_1_ignored(hass): assert order == ["cloud", "an_after_dep", "normal_integration"] +@pytest.mark.parametrize("load_registries", [False]) +async def test_setup_frontend_before_recorder(hass): + """Test frontend is setup before recorder.""" + order = [] + + def gen_domain_setup(domain): + async def async_setup(hass, config): + order.append(domain) + return True + + return async_setup + + mock_integration( + hass, + MockModule( + domain="normal_integration", + async_setup=gen_domain_setup("normal_integration"), + partial_manifest={"after_dependencies": ["an_after_dep"]}, + ), + ) + mock_integration( + hass, + MockModule( + domain="an_after_dep", + async_setup=gen_domain_setup("an_after_dep"), + ), + ) + mock_integration( + hass, + MockModule( + domain="frontend", + async_setup=gen_domain_setup("frontend"), + partial_manifest={ + "dependencies": ["http"], + "after_dependencies": ["an_after_dep"], + }, + ), + ) + mock_integration( + hass, + MockModule( + domain="http", + async_setup=gen_domain_setup("http"), + ), + ) + mock_integration( + hass, + MockModule( + domain="recorder", + async_setup=gen_domain_setup("recorder"), + ), + ) + + await bootstrap._async_set_up_integrations( + hass, + { + "frontend": {}, + "http": {}, + "recorder": {}, + "normal_integration": {}, + "an_after_dep": {}, + }, + ) + + assert "frontend" in hass.config.components + assert "normal_integration" in hass.config.components + assert "recorder" in hass.config.components + assert order == [ + "http", + "frontend", + "recorder", + "an_after_dep", + "normal_integration", + ] + + @pytest.mark.parametrize("load_registries", [False]) async def test_setup_after_deps_via_platform(hass): """Test after_dependencies set up via platform.""" From e9697872c8ebb8e0b41c5e7bf78dab6d3b58afcf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Jul 2022 16:02:10 +0200 Subject: [PATCH 630/820] Fix small homekit type error (#75617) --- homeassistant/components/homekit/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 5d7e647e466..6852ff4fa9f 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -8,7 +8,6 @@ import ipaddress import logging import os from typing import Any, cast -from uuid import UUID from aiohttp import web from pyhap.const import STANDALONE_AID @@ -510,7 +509,7 @@ class HomeKit: self.bridge: HomeBridge | None = None - def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: UUID) -> None: + def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: """Set up bridge and accessory driver.""" persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) From 148f96351052b0a4ba31e7d15dad16d7639e1ceb Mon Sep 17 00:00:00 2001 From: "Diogo F. Andrade Murteira" Date: Fri, 22 Jul 2022 17:03:02 +0100 Subject: [PATCH 631/820] Add Switchbot hygrometers (#75325) * Switchbot add support for hygrometers * Update CODEOWNERS * Improve debug * Remove redundant mention to temp unit * Adopt FlowResultType * Modify SwitchBot data within coordinator * Increase logging for switchbot sensor * Revert "Increase logging for switchbot sensor" This reverts commit d8b377429c562fc7044a3c98a6e976e4cd71847e. Co-authored-by: J. Nick Koston --- CODEOWNERS | 4 +-- .../components/switchbot/__init__.py | 2 ++ .../components/switchbot/config_flow.py | 2 ++ homeassistant/components/switchbot/const.py | 7 +++- .../components/switchbot/coordinator.py | 13 ++++++- .../components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/sensor.py | 11 ++++++ tests/components/switchbot/__init__.py | 6 ++++ tests/components/switchbot/conftest.py | 15 ++++++++ .../components/switchbot/test_config_flow.py | 35 ++++++++++++++++++- 10 files changed, 91 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 792a6302b79..3b38b6e1a5a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1038,8 +1038,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core -/homeassistant/components/switchbot/ @danielhiversen @RenierM26 -/tests/components/switchbot/ @danielhiversen @RenierM26 +/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas +/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode /homeassistant/components/switchmate/ @danielhiversen diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 0a3d01f3382..d4418685cff 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant from .const import ( ATTR_BOT, ATTR_CURTAIN, + ATTR_HYGROMETER, COMMON_OPTIONS, CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, @@ -26,6 +27,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], + ATTR_HYGROMETER: [Platform.SENSOR], } diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 362f3b01ae7..b35ba052d0a 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -87,6 +87,8 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): try: self._discovered_devices = await self._get_switchbots() + for device in self._discovered_devices.values(): + _LOGGER.debug("Found %s", device) except NotConnectedError: return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index b1587e97c10..a363c030eb1 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -5,8 +5,13 @@ MANUFACTURER = "switchbot" # Config Attributes ATTR_BOT = "bot" ATTR_CURTAIN = "curtain" +ATTR_HYGROMETER = "hygrometer" DEFAULT_NAME = "Switchbot" -SUPPORTED_MODEL_TYPES = {"WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN} +SUPPORTED_MODEL_TYPES = { + "WoHand": ATTR_BOT, + "WoCurtain": ATTR_CURTAIN, + "WoSensorTH": ATTR_HYGROMETER, +} # Config Defaults DEFAULT_RETRY_COUNT = 3 diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index e8e2e240dc6..4a4831f2cdb 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -14,6 +14,14 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +def flatten_sensors_data(sensor): + """Deconstruct SwitchBot library temp object C/Fº readings from dictionary.""" + if "temp" in sensor["data"]: + sensor["data"]["temperature"] = sensor["data"]["temp"]["c"] + + return sensor + + class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching switchbot data.""" @@ -47,4 +55,7 @@ class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): if not switchbot_data: raise UpdateFailed("Unable to fetch switchbot services data") - return switchbot_data + return { + identifier: flatten_sensors_data(sensor) + for identifier, sensor in switchbot_data.items() + } diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index ccce534c6df..76b43628ab3 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "requirements": ["PySwitchbot==0.14.1"], "config_flow": true, - "codeowners": ["@danielhiversen", "@RenierM26"], + "codeowners": ["@danielhiversen", "@RenierM26", "@murtas"], "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], "iot_class": "local_polling", "loggers": ["switchbot"] diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 759a504d19a..0bc7fe8a3b2 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -43,6 +44,16 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { native_unit_of_measurement="Level", device_class=SensorDeviceClass.ILLUMINANCE, ), + "humidity": SensorEntityDescription( + key="humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + ), + "temperature": SensorEntityDescription( + key="temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), } diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 376406ac50c..8f1501bfa9a 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -26,6 +26,12 @@ USER_INPUT_CURTAIN = { CONF_MAC: "e7:89:43:90:90:90", } +USER_INPUT_SENSOR = { + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_MAC: "c0:ce:b0:d4:26:be", +} + USER_INPUT_UNSUPPORTED_DEVICE = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 550aeb08082..2e6421f22a4 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -72,6 +72,19 @@ class MocGetSwitchbotDevices: }, "modelName": "WoCurtain", } + self._sensor_data = { + "mac_address": "c0:ce:b0:d4:26:be", + "isEncrypted": False, + "data": { + "temp": {"c": 21.6, "f": 70.88}, + "fahrenheit": False, + "humidity": 73, + "battery": 100, + "rssi": -58, + }, + "model": "T", + "modelName": "WoSensorTH", + } self._unsupported_device = { "mac_address": "test", "isEncrypted": False, @@ -97,6 +110,8 @@ class MocGetSwitchbotDevices: return self._unsupported_device if mac == "e7:89:43:90:90:90": return self._curtain_all_services_data + if mac == "c0:ce:b0:d4:26:be": + return self._sensor_data return None diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 814e79cf591..aa1adb3a16e 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -11,7 +11,13 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.data_entry_flow import FlowResultType -from . import USER_INPUT, USER_INPUT_CURTAIN, init_integration, patch_async_setup_entry +from . import ( + USER_INPUT, + USER_INPUT_CURTAIN, + USER_INPUT_SENSOR, + init_integration, + patch_async_setup_entry, +) DOMAIN = "switchbot" @@ -71,6 +77,33 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 + # test sensor device creation. + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT_SENSOR, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-name" + assert result["data"] == { + CONF_MAC: "c0:ce:b0:d4:26:be", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "hygrometer", + } + + assert len(mock_setup_entry.mock_calls) == 1 + # tests abort if no unconfigured devices are found. result = await hass.config_entries.flow.async_init( From c05905ebda3921874ce81683b7cbf4e5ce0edfbb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 22 Jul 2022 19:09:02 +0200 Subject: [PATCH 632/820] Remove callback decorator from coroutine functions (#75626) * Remove callback decorator from coroutine functions * Remove some more callback decorators --- homeassistant/components/fitbit/sensor.py | 3 +-- homeassistant/components/fritz/common.py | 1 - homeassistant/components/generic_hygrostat/humidifier.py | 3 --- homeassistant/components/group/__init__.py | 1 - homeassistant/components/motioneye/__init__.py | 1 - homeassistant/components/mqtt/client.py | 1 - homeassistant/components/mqtt/tag.py | 3 +-- homeassistant/components/network/network.py | 1 - homeassistant/components/philips_js/__init__.py | 1 - homeassistant/components/switcher_kis/__init__.py | 1 - homeassistant/components/zwave_js/device_condition.py | 1 - tests/components/logbook/test_websocket_api.py | 3 +-- 12 files changed, 3 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 48b05482dec..3165843a23d 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -25,7 +25,7 @@ from homeassistant.const import ( CONF_CLIENT_SECRET, CONF_UNIT_SYSTEM, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level @@ -279,7 +279,6 @@ class FitbitAuthCallbackView(HomeAssistantView): self.add_entities = add_entities self.oauth = oauth - @callback async def get(self, request: Request) -> str: """Finish OAuth callback request.""" hass: HomeAssistant = request.app["hass"] diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 4a01367cc20..d748bdcf7dd 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -240,7 +240,6 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): self.device_conn_type, "GetInfo" ).get("NewEnable") - @callback async def _async_update_data(self) -> None: """Update FritzboxTools data.""" try: diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index fa8b05b5ef8..8072c76ea07 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -173,7 +173,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): if self._keep_alive: async_track_time_interval(self.hass, self._async_operate, self._keep_alive) - @callback async def _async_startup(event): """Init on startup.""" sensor_state = self.hass.states.get(self._sensor_entity_id) @@ -309,7 +308,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): # Get default humidity from super class return super().max_humidity - @callback async def _async_sensor_changed(self, entity_id, old_state, new_state): """Handle ambient humidity changes.""" if new_state is None: @@ -328,7 +326,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): await self._async_operate() await self.async_update_ha_state() - @callback async def _async_sensor_not_responding(self, now=None): """Handle sensor stale event.""" diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 943b15dd870..04bc109d15b 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -574,7 +574,6 @@ class Group(Entity): return group @staticmethod - @callback async def async_create_group( hass: HomeAssistant, name: str, diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 462f5ef03d2..7c87dda1bd2 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -328,7 +328,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "motionEye", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - @callback async def async_update_data() -> dict[str, Any] | None: try: return await client.async_get_cameras() diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 6e6f67e4e7a..81142fadb87 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -343,7 +343,6 @@ class MQTT: self.init_client() - @callback async def async_stop_mqtt(_event: Event): """Stop MQTT component.""" await self.async_disconnect() diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 9452d5fc259..dc4cc0e109d 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_PLATFORM, CONF_VALUE_TEMPLATE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -128,7 +128,6 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): async def subscribe_topics(self) -> None: """Subscribe to MQTT topics.""" - @callback async def tag_scanned(msg: ReceiveMessage) -> None: tag_id = self._value_template(msg.payload, "").strip() if not tag_id: # No output from template, ignore diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index e9542ec2d54..0b90023bfd4 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -22,7 +22,6 @@ _LOGGER = logging.getLogger(__name__) @singleton(DATA_NETWORK) -@callback async def async_get_network(hass: HomeAssistant) -> Network: """Get network singleton.""" network = Network(hass) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 40e803e4b35..9e574e69f90 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -187,7 +187,6 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): super()._unschedule_refresh() self._async_notify_stop() - @callback async def _async_update_data(self): """Fetch the latest data from the source.""" try: diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index e0f328e9312..31273dce23d 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -106,7 +106,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_start_bridge(hass, on_device_data_callback) - @callback async def stop_bridge(event: Event) -> None: await async_stop_bridge(hass) diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 7775995437a..c42b5af71c4 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -196,7 +196,6 @@ def async_condition_from_config( raise HomeAssistantError(f"Unhandled condition type {condition_type}") -@callback async def async_get_condition_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 6c4908a2ad5..ec4f2183a9a 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -27,7 +27,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.setup import async_setup_component @@ -51,7 +51,6 @@ def set_utc(hass): hass.config.set_time_zone("UTC") -@callback async def _async_mock_logbook_platform(hass: HomeAssistant) -> None: class MockLogbookPlatform: """Mock a logbook platform.""" From 20b6c4c48e9fc1b33eefed1844afd64ae19ab064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 12:37:25 -0500 Subject: [PATCH 633/820] Fix recorder hanging at start (#75627) --- homeassistant/components/recorder/core.py | 27 +++++++++++-------- .../components/recorder/migration.py | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 92e10b47126..30ece9e98a5 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -436,7 +436,7 @@ class Recorder(threading.Thread): self.async_db_connected.set_result(True) @callback - def async_set_recorder_ready(self) -> None: + def async_set_db_ready(self) -> None: """Database live and ready for use. Called after non-live migration steps are finished. @@ -577,14 +577,19 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_connection_success) - # If shutdown happened before Home Assistant finished starting - if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: - self.migration_in_progress = False - # Make sure we cleanly close the run if - # we restart before startup finishes - self._shutdown() - self.hass.add_job(self.async_set_recorder_ready) - return + if self.migration_is_live or schema_is_current: + # If the migrate is live or the schema is current, we need to + # wait for startup to complete. If its not live, we need to continue + # on. + self.hass.add_job(self.async_set_db_ready) + # If shutdown happened before Home Assistant finished starting + if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: + self.migration_in_progress = False + # Make sure we cleanly close the run if + # we restart before startup finishes + self._shutdown() + self.hass.add_job(self.async_set_db_ready) + return # We wait to start the migration until startup has finished # since it can be cpu intensive and we do not want it to compete @@ -604,11 +609,11 @@ class Recorder(threading.Thread): "Database Migration Failed", "recorder_database_migration", ) - self.hass.add_job(self.async_set_recorder_ready) + self.hass.add_job(self.async_set_db_ready) self._shutdown() return - self.hass.add_job(self.async_set_recorder_ready) + self.hass.add_job(self.async_set_db_ready) _LOGGER.debug("Recorder processing the queue") self.hass.add_job(self._async_set_recorder_ready_migration_done) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index de6fd8f01fe..6e4a67c9da5 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -99,7 +99,7 @@ def migrate_schema( if live_migration(version) and not db_ready: db_ready = True instance.migration_is_live = True - hass.add_job(instance.async_set_recorder_ready) + hass.add_job(instance.async_set_db_ready) new_version = version + 1 _LOGGER.info("Upgrading recorder db schema to version %s", new_version) _apply_update(hass, engine, session_maker, new_version, current_version) From 38bccadaa6c2669d34731e84c2cdb6cd2f8d3e1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 13:19:53 -0500 Subject: [PATCH 634/820] Add support for setting up and removing bluetooth in the UI (#75600) Co-authored-by: Paulus Schoutsen --- .../components/bluetooth/__init__.py | 135 +++- .../components/bluetooth/config_flow.py | 37 + homeassistant/components/bluetooth/const.py | 1 + .../components/bluetooth/manifest.json | 3 +- homeassistant/components/bluetooth/models.py | 15 +- .../components/bluetooth/strings.json | 6 + .../components/bluetooth/translations/en.json | 6 + homeassistant/components/bluetooth/usage.py | 13 +- .../components/default_config/manifest.json | 1 + homeassistant/generated/config_flows.py | 1 + homeassistant/package_constraints.txt | 2 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../components/bluetooth/test_config_flow.py | 106 +++ tests/components/bluetooth/test_init.py | 694 +++++++++++------- .../test_passive_update_coordinator.py | 12 +- tests/components/bluetooth/test_usage.py | 21 +- .../bluetooth_le_tracker/conftest.py | 7 +- tests/components/default_config/test_init.py | 2 +- tests/components/inkbird/conftest.py | 2 +- tests/components/sensorpush/conftest.py | 4 +- tests/conftest.py | 20 +- 22 files changed, 755 insertions(+), 339 deletions(-) create mode 100644 homeassistant/components/bluetooth/config_flow.py create mode 100644 tests/components/bluetooth/test_config_flow.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 46d1e5e8332..3c7f2393257 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta from enum import Enum import fnmatch import logging +import platform from typing import Final, TypedDict, Union from bleak import BleakError @@ -35,7 +36,7 @@ from homeassistant.loader import ( from . import models from .const import DOMAIN from .models import HaBleakScanner -from .usage import install_multiple_bleak_catcher +from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher _LOGGER = logging.getLogger(__name__) @@ -115,6 +116,15 @@ BluetoothCallback = Callable[ ] +@hass_callback +def async_get_scanner(hass: HomeAssistant) -> HaBleakScanner: + """Return a HaBleakScanner.""" + if DOMAIN not in hass.data: + raise RuntimeError("Bluetooth integration not loaded") + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_get_scanner() + + @hass_callback def async_discovered_service_info( hass: HomeAssistant, @@ -178,14 +188,62 @@ def async_track_unavailable( return manager.async_track_unavailable(callback, address) +async def _async_has_bluetooth_adapter() -> bool: + """Return if the device has a bluetooth adapter.""" + if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware + return True + if platform.system() == "Windows": # We don't have a good way to detect on windows + return False + from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel + get_bluetooth_adapters, + ) + + return bool(await get_bluetooth_adapters()) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matchers = await async_get_bluetooth(hass) - bluetooth_discovery = BluetoothManager( - hass, integration_matchers, BluetoothScanningMode.PASSIVE - ) - await bluetooth_discovery.async_setup() - hass.data[DOMAIN] = bluetooth_discovery + manager = BluetoothManager(hass, integration_matchers) + manager.async_setup() + hass.data[DOMAIN] = manager + # The config entry is responsible for starting the manager + # if its enabled + + if hass.config_entries.async_entries(DOMAIN): + return True + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} + ) + ) + elif await _async_has_bluetooth_adapter(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + ) + return True + + +async def async_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Set up the bluetooth integration from a config entry.""" + manager: BluetoothManager = hass.data[DOMAIN] + await manager.async_start(BluetoothScanningMode.ACTIVE) + return True + + +async def async_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Unload a config entry.""" + manager: BluetoothManager = hass.data[DOMAIN] + await manager.async_stop() return True @@ -241,11 +299,9 @@ class BluetoothManager: self, hass: HomeAssistant, integration_matchers: list[BluetoothMatcher], - scanning_mode: BluetoothScanningMode, ) -> None: """Init bluetooth discovery.""" self.hass = hass - self.scanning_mode = scanning_mode self._integration_matchers = integration_matchers self.scanner: HaBleakScanner | None = None self._cancel_device_detected: CALLBACK_TYPE | None = None @@ -258,19 +314,27 @@ class BluetoothManager: # an LRU to avoid memory issues. self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES) - async def async_setup(self) -> None: + @hass_callback + def async_setup(self) -> None: + """Set up the bluetooth manager.""" + models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() + + @hass_callback + def async_get_scanner(self) -> HaBleakScanner: + """Get the scanner.""" + assert self.scanner is not None + return self.scanner + + async def async_start(self, scanning_mode: BluetoothScanningMode) -> None: """Set up BT Discovery.""" + assert self.scanner is not None try: - self.scanner = HaBleakScanner( - scanning_mode=SCANNING_MODE_TO_BLEAK[self.scanning_mode] + self.scanner.async_setup( + scanning_mode=SCANNING_MODE_TO_BLEAK[scanning_mode] ) except (FileNotFoundError, BleakError) as ex: - _LOGGER.warning( - "Could not create bluetooth scanner (is bluetooth present and enabled?): %s", - ex, - ) - return - install_multiple_bleak_catcher(self.scanner) + raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex + install_multiple_bleak_catcher() self.async_setup_unavailable_tracking() # We have to start it right away as some integrations might # need it straight away. @@ -279,8 +343,11 @@ class BluetoothManager: self._cancel_device_detected = self.scanner.async_register_callback( self._device_detected, {} ) + try: + await self.scanner.start() + except (FileNotFoundError, BleakError) as ex: + raise RuntimeError(f"Failed to start Bluetooth: {ex}") from ex self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) - await self.scanner.start() @hass_callback def async_setup_unavailable_tracking(self) -> None: @@ -289,8 +356,8 @@ class BluetoothManager: @hass_callback def _async_check_unavailable(now: datetime) -> None: """Watch for unavailable devices.""" - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HA_BLEAK_SCANNER + scanner = self.scanner + assert scanner is not None history = set(scanner.history) active = {device.address for device in scanner.discovered_devices} disappeared = history.difference(active) @@ -406,8 +473,8 @@ class BluetoothManager: if ( matcher and (address := matcher.get(ADDRESS)) - and models.HA_BLEAK_SCANNER - and (device_adv_data := models.HA_BLEAK_SCANNER.history.get(address)) + and self.scanner + and (device_adv_data := self.scanner.history.get(address)) ): try: callback( @@ -424,31 +491,25 @@ class BluetoothManager: @hass_callback def async_ble_device_from_address(self, address: str) -> BLEDevice | None: """Return the BLEDevice if present.""" - if models.HA_BLEAK_SCANNER and ( - ble_adv := models.HA_BLEAK_SCANNER.history.get(address) - ): + if self.scanner and (ble_adv := self.scanner.history.get(address)): return ble_adv[0] return None @hass_callback def async_address_present(self, address: str) -> bool: """Return if the address is present.""" - return bool( - models.HA_BLEAK_SCANNER and address in models.HA_BLEAK_SCANNER.history - ) + return bool(self.scanner and address in self.scanner.history) @hass_callback def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: """Return if the address is present.""" - if models.HA_BLEAK_SCANNER: - history = models.HA_BLEAK_SCANNER.history - return [ - BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) - for device_adv in history.values() - ] - return [] + assert self.scanner is not None + return [ + BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) + for device_adv in self.scanner.history.values() + ] - async def async_stop(self, event: Event) -> None: + async def async_stop(self, event: Event | None = None) -> None: """Stop bluetooth discovery.""" if self._cancel_device_detected: self._cancel_device_detected() @@ -458,4 +519,4 @@ class BluetoothManager: self._cancel_unavailable_tracking = None if self.scanner: await self.scanner.stop() - models.HA_BLEAK_SCANNER = None + uninstall_multiple_bleak_catcher() diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py new file mode 100644 index 00000000000..2193170810f --- /dev/null +++ b/homeassistant/components/bluetooth/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow to configure the Bluetooth integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Bluetooth.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + return await self.async_step_enable_bluetooth() + + async def async_step_enable_bluetooth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user or import.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + if user_input is not None: + return self.async_create_entry(title=DEFAULT_NAME, data={}) + + return self.async_show_form(step_id="enable_bluetooth") + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_enable_bluetooth(user_input) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index ca5777ccdc2..1e577f6064a 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -1,3 +1,4 @@ """Constants for the Bluetooth integration.""" DOMAIN = "bluetooth" +DEFAULT_NAME = "Bluetooth" diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 0cc11ee14b3..551bb1c3733 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.14.3"], + "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.1"], "codeowners": ["@bdraco"], + "config_flow": true, "iot_class": "local_push" } diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index ffb0ad107ec..e1d15c27243 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -48,13 +48,24 @@ def _dispatch_callback( class HaBleakScanner(BleakScanner): # type: ignore[misc] """BleakScanner that cannot be stopped.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__( # pylint: disable=super-init-not-called + self, *args: Any, **kwargs: Any + ) -> None: """Initialize the BleakScanner.""" self._callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] self.history: dict[str, tuple[BLEDevice, AdvertisementData]] = {} - super().__init__(*args, **kwargs) + # Init called later in async_setup if we are enabling the scanner + # since init has side effects that can throw exceptions + self._setup = False + + @hass_callback + def async_setup(self, *args: Any, **kwargs: Any) -> None: + """Deferred setup of the BleakScanner since __init__ has side effects.""" + if not self._setup: + super().__init__(*args, **kwargs) + self._setup = True @hass_callback def async_register_callback( diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index 925e9c512cc..328a001ad96 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -2,6 +2,9 @@ "config": { "flow_title": "{name}", "step": { + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, "user": { "description": "Choose a device to setup", "data": { @@ -11,6 +14,9 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index f75dc2603db..85019bdd689 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, "user": { "data": { "address": "Device" diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index e305576f97f..da5d062a36f 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -3,11 +3,16 @@ from __future__ import annotations import bleak -from . import models -from .models import HaBleakScanner, HaBleakScannerWrapper +from .models import HaBleakScannerWrapper + +ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner -def install_multiple_bleak_catcher(hass_bleak_scanner: HaBleakScanner) -> None: +def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" - models.HA_BLEAK_SCANNER = hass_bleak_scanner bleak.BleakScanner = HaBleakScannerWrapper + + +def uninstall_multiple_bleak_catcher() -> None: + """Unwrap the bleak classes.""" + bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 1742092cc70..3cb9e60a278 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -5,6 +5,7 @@ "dependencies": [ "application_credentials", "automation", + "bluetooth", "cloud", "counter", "dhcp", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 33e34035b34..2c7732fee8d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -47,6 +47,7 @@ FLOWS = { "balboa", "blebox", "blink", + "bluetooth", "bmw_connected_drive", "bond", "bosch_shc", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d787cec62f2..ad640ff596b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,6 +10,8 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 +bleak==0.14.3 +bluetooth-adapters==0.1.1 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index c17e031f07a..15c62c63c2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,6 +424,9 @@ blockchain==1.4.4 # homeassistant.components.zengge # bluepy==1.3.0 +# homeassistant.components.bluetooth +bluetooth-adapters==0.1.1 + # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8745afe1e64..742d6eb0f3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -334,6 +334,9 @@ blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 +# homeassistant.components.bluetooth +bluetooth-adapters==0.1.1 + # homeassistant.components.bond bond-async==0.1.22 diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py new file mode 100644 index 00000000000..550f5a583d7 --- /dev/null +++ b/tests/components/bluetooth/test_config_flow.py @@ -0,0 +1,106 @@ +"""Test the bluetooth config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.bluetooth.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_async_step_user(hass): + """Test setting up manually.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "enable_bluetooth" + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Bluetooth" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_user_only_allows_one(hass): + """Test setting up manually with an existing entry.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_integration_discovery(hass): + """Test setting up from integration discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "enable_bluetooth" + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Bluetooth" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_integration_discovery_already_exists(hass): + """Test setting up from integration discovery when an entry already exists.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_import(hass): + """Test setting up from integration discovery.""" + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Bluetooth" + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_import_already_exists(hass): + """Test setting up from yaml when an entry already exists.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e76ad559305..8aef5f3ddbb 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice +import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( @@ -11,6 +12,7 @@ from homeassistant.components.bluetooth import ( UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, + async_get_scanner, async_track_unavailable, models, ) @@ -19,10 +21,10 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed -async def test_setup_and_stop(hass, mock_bleak_scanner_start): +async def test_setup_and_stop(hass, mock_bleak_scanner_start, enable_bluetooth): """Test we and setup and stop the scanner.""" mock_bt = [ {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} @@ -47,33 +49,57 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} ] with patch( - "homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError + "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + side_effect=BleakError, ) as mock_ha_bleak_scanner, patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object( - hass.config_entries.flow, "async_init" ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert len(mock_ha_bleak_scanner.mock_calls) == 1 - assert "Could not create bluetooth scanner" in caplog.text + assert "Failed to initialize Bluetooth" in caplog.text + + +async def test_setup_and_stop_broken_bluetooth(hass, caplog): + """Test we fail gracefully when bluetooth/dbus is broken.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BleakError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "Failed to start Bluetooth" in caplog.text + assert len(bluetooth.async_discovered_service_info(hass)) == 0 async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError - ) as mock_ha_bleak_scanner, patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + side_effect=FileNotFoundError, + ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object( - hass.config_entries.flow, "async_init" ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} @@ -83,13 +109,14 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert len(mock_ha_bleak_scanner.mock_calls) == 1 - assert "Could not create bluetooth scanner" in caplog.text + assert "Failed to initialize Bluetooth" in caplog.text assert not bluetooth.async_discovered_service_info(hass) assert not bluetooth.async_address_present(hass, "aa:bb:bb:dd:ee:ff") -async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): +async def test_discovery_match_by_service_uuid( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test bluetooth discovery match by service_uuid.""" mock_bt = [ {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} @@ -108,7 +135,7 @@ async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + async_get_scanner(hass)._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -118,7 +145,7 @@ async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -130,10 +157,13 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -142,7 +172,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + async_get_scanner(hass)._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -150,7 +180,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -170,10 +200,13 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( ] with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -186,7 +219,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( manufacturer_data={76: b"\x06\x02\x03\x99"}, ) - models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) + async_get_scanner(hass)._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -194,7 +227,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( mock_config_flow.reset_mock() # 2nd discovery should not generate another flow - models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) + async_get_scanner(hass)._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -205,7 +238,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} ) - models.HA_BLEAK_SCANNER._callback(not_hkc_device, not_hkc_adv) + async_get_scanner(hass)._callback(not_hkc_device, not_hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -214,14 +247,14 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} ) - models.HA_BLEAK_SCANNER._callback(not_apple_device, not_apple_adv) + async_get_scanner(hass)._callback(not_apple_device, not_apple_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): - """Test the async_discovered_device_api.""" + """Test the async_discovered_device API.""" mock_bt = [] with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -231,84 +264,86 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): ): assert not bluetooth.async_discovered_service_info(hass) assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") - assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert len(mock_bleak_scanner_start.mock_calls) == 1 + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() - assert not bluetooth.async_discovered_service_info(hass) + assert len(mock_bleak_scanner_start.mock_calls) == 1 - wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") - wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - wrong_device_went_unavailable = False - switchbot_device_went_unavailable = False + assert not bluetooth.async_discovered_service_info(hass) - @callback - def _wrong_device_unavailable_callback(_address: str) -> None: - """Wrong device unavailable callback.""" - nonlocal wrong_device_went_unavailable - wrong_device_went_unavailable = True - raise ValueError("blow up") + wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + async_get_scanner(hass)._callback(wrong_device, wrong_adv) + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False - @callback - def _switchbot_device_unavailable_callback(_address: str) -> None: - """Switchbot device unavailable callback.""" - nonlocal switchbot_device_went_unavailable - switchbot_device_went_unavailable = True + @callback + def _wrong_device_unavailable_callback(_address: str) -> None: + """Wrong device unavailable callback.""" + nonlocal wrong_device_went_unavailable + wrong_device_went_unavailable = True + raise ValueError("blow up") - wrong_device_unavailable_cancel = async_track_unavailable( - hass, _wrong_device_unavailable_callback, wrong_device.address - ) - switchbot_device_unavailable_cancel = async_track_unavailable( - hass, _switchbot_device_unavailable_callback, switchbot_device.address - ) + @callback + def _switchbot_device_unavailable_callback(_address: str) -> None: + """Switchbot device unavailable callback.""" + nonlocal switchbot_device_went_unavailable + switchbot_device_went_unavailable = True - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) - ) - await hass.async_block_till_done() + wrong_device_unavailable_cancel = async_track_unavailable( + hass, _wrong_device_unavailable_callback, wrong_device.address + ) + switchbot_device_unavailable_cancel = async_track_unavailable( + hass, _switchbot_device_unavailable_callback, switchbot_device.address + ) - service_infos = bluetooth.async_discovered_service_info(hass) - assert switchbot_device_went_unavailable is False - assert wrong_device_went_unavailable is True + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() - # See the devices again - models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - # Cancel the callbacks - wrong_device_unavailable_cancel() - switchbot_device_unavailable_cancel() - wrong_device_went_unavailable = False - switchbot_device_went_unavailable = False + service_infos = bluetooth.async_discovered_service_info(hass) + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is True - # Verify the cancel is effective - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) - ) - await hass.async_block_till_done() - assert switchbot_device_went_unavailable is False - assert wrong_device_went_unavailable is False + # See the devices again + async_get_scanner(hass)._callback(wrong_device, wrong_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + # Cancel the callbacks + wrong_device_unavailable_cancel() + switchbot_device_unavailable_cancel() + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False - assert len(service_infos) == 1 - # wrong_name should not appear because bleak no longer sees it - assert service_infos[0].name == "wohand" - assert service_infos[0].source == SOURCE_LOCAL - assert isinstance(service_infos[0].device, BLEDevice) - assert isinstance(service_infos[0].advertisement, AdvertisementData) + # Verify the cancel is effective + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is False - assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False - assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True + assert len(service_infos) == 1 + # wrong_name should not appear because bleak no longer sees it + assert service_infos[0].name == "wohand" + assert service_infos[0].source == SOURCE_LOCAL + assert isinstance(service_infos[0].device, BLEDevice) + assert isinstance(service_infos[0].advertisement, AdvertisementData) + + assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False + assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True -async def test_register_callbacks(hass, mock_bleak_scanner_start): +async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetooth): """Test registering a callback.""" mock_bt = [] callbacks = [] @@ -347,25 +382,25 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() assert len(callbacks) == 3 @@ -389,7 +424,9 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): assert service_info.manufacturer_id is None -async def test_register_callback_by_address(hass, mock_bleak_scanner_start): +async def test_register_callback_by_address( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test registering a callback by address.""" mock_bt = [] callbacks = [] @@ -404,10 +441,13 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_start): with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -427,25 +467,25 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_start): service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() # Now register again with a callback that fails to @@ -475,121 +515,133 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_start): assert service_info.manufacturer_id == 89 -async def test_wrapped_instance_with_filter(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_with_filter( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - detected = [] + detected = [] - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - detected.append((device, advertisement_data)) + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) - empty_device = BLEDevice("11:22:33:44:55:66", "empty") - empty_adv = AdvertisementData(local_name="empty") + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper( - filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} - ) - scanner.register_detection_callback(_device_detected) + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) - mock_discovered = [MagicMock()] - type(models.HA_BLEAK_SCANNER).discovered_devices = mock_discovered - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() + mock_discovered = [MagicMock()] + type(async_get_scanner(hass)).discovered_devices = mock_discovered + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() - discovered = await scanner.discover(timeout=0) - assert len(discovered) == 1 - assert discovered == mock_discovered - assert len(detected) == 1 + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 1 + assert discovered == mock_discovered + assert len(detected) == 1 - scanner.register_detection_callback(_device_detected) - # We should get a reply from the history when we register again - assert len(detected) == 2 - scanner.register_detection_callback(_device_detected) - # We should get a reply from the history when we register again - assert len(detected) == 3 + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 2 + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 3 - type(models.HA_BLEAK_SCANNER).discovered_devices = [] - discovered = await scanner.discover(timeout=0) - assert len(discovered) == 0 - assert discovered == [] + type(async_get_scanner(hass)).discovered_devices = [] + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 0 + assert discovered == [] - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - assert len(detected) == 4 + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + assert len(detected) == 4 - # The filter we created in the wrapped scanner with should be respected - # and we should not get another callback - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) - assert len(detected) == 4 + # The filter we created in the wrapped scanner with should be respected + # and we should not get another callback + async_get_scanner(hass)._callback(empty_device, empty_adv) + assert len(detected) == 4 -async def test_wrapped_instance_with_service_uuids(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_with_service_uuids( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - detected = [] + detected = [] - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - detected.append((device, advertisement_data)) + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) - empty_device = BLEDevice("11:22:33:44:55:66", "empty") - empty_adv = AdvertisementData(local_name="empty") + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper( - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] - ) - scanner.register_detection_callback(_device_detected) + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) - type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] - for _ in range(2): - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() + type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + for _ in range(2): + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() - assert len(detected) == 2 + assert len(detected) == 2 - # The UUIDs list we created in the wrapped scanner with should be respected - # and we should not get another callback - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) - assert len(detected) == 2 + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + async_get_scanner(hass)._callback(empty_device, empty_adv) + assert len(detected) == 2 -async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_with_broken_callbacks( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test broken callbacks do not cause the scanner to fail.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] @@ -597,158 +649,173 @@ async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_s assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - detected = [] + detected = [] - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - if detected: - raise ValueError - detected.append((device, advertisement_data)) + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + if detected: + raise ValueError + detected.append((device, advertisement_data)) - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper( - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] - ) - scanner.register_detection_callback(_device_detected) + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() - assert len(detected) == 1 + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + assert len(detected) == 1 -async def test_wrapped_instance_changes_uuids(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_changes_uuids( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test consumers can use the wrapped instance can change the uuids later.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() + detected = [] - detected = [] + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - detected.append((device, advertisement_data)) + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) - empty_device = BLEDevice("11:22:33:44:55:66", "empty") - empty_adv = AdvertisementData(local_name="empty") + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper() - scanner.set_scanning_filter(service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]) - scanner.register_detection_callback(_device_detected) + type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + for _ in range(2): + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() - type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] - for _ in range(2): - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() + assert len(detected) == 2 - assert len(detected) == 2 - - # The UUIDs list we created in the wrapped scanner with should be respected - # and we should not get another callback - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) - assert len(detected) == 2 + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + async_get_scanner(hass)._callback(empty_device, empty_adv) + assert len(detected) == 2 -async def test_wrapped_instance_changes_filters(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_changes_filters( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test consumers can use the wrapped instance can change the filter later.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() + detected = [] - detected = [] + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - detected.append((device, advertisement_data)) + switchbot_device = BLEDevice("44:44:33:11:23:42", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:62", "empty") + empty_adv = AdvertisementData(local_name="empty") - switchbot_device = BLEDevice("44:44:33:11:23:42", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) - empty_device = BLEDevice("11:22:33:44:55:62", "empty") - empty_adv = AdvertisementData(local_name="empty") + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper() - scanner.set_scanning_filter( - filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} - ) - scanner.register_detection_callback(_device_detected) + type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + for _ in range(2): + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() - type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] - for _ in range(2): - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() + assert len(detected) == 2 - assert len(detected) == 2 - - # The UUIDs list we created in the wrapped scanner with should be respected - # and we should not get another callback - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) - assert len(detected) == 2 + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + async_get_scanner(hass)._callback(empty_device, empty_adv) + assert len(detected) == 2 async def test_wrapped_instance_unsupported_filter( - hass, mock_bleak_scanner_start, caplog + hass, mock_bleak_scanner_start, caplog, enable_bluetooth ): """Test we want when their filter is ineffective.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper() - scanner.set_scanning_filter( - filters={ - "unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - "DuplicateData": True, - } - ) - assert "Only UUIDs filters are supported" in caplog.text + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={ + "unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + "DuplicateData": True, + } + ) + assert "Only UUIDs filters are supported" in caplog.text async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): @@ -778,7 +845,7 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert ( @@ -789,3 +856,82 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): assert ( bluetooth.async_ble_device_from_address(hass, "00:66:33:22:11:22") is None ) + + +async def test_setup_without_bluetooth_in_configuration_yaml(hass, mock_bluetooth): + """Test setting up without bluetooth in configuration.yaml does not create the config entry.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + + +async def test_setup_with_bluetooth_in_configuration_yaml(hass, mock_bluetooth): + """Test setting up with bluetooth in configuration.yaml creates the config entry.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) + await hass.async_block_till_done() + assert hass.config_entries.async_entries(bluetooth.DOMAIN) + + +async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_bluetooth): + """Test we can setup and unsetup bluetooth.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry.add_to_hass(hass) + for _ in range(2): + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_auto_detect_bluetooth_adapters_linux(hass): + """Test we auto detect bluetooth adapters on linux.""" + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value={"hci0"} + ), patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + + +async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): + """Test we auto detect bluetooth adapters on linux with no adapters found.""" + with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()), patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 + + +async def test_auto_detect_bluetooth_adapters_macos(hass): + """Test we auto detect bluetooth adapters on macos.""" + with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Darwin" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + + +async def test_no_auto_detect_bluetooth_adapters_windows(hass): + """Test we auto detect bluetooth adapters on windows.""" + with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Windows" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 + + +async def test_raising_runtime_error_when_no_bluetooth(hass): + """Test we raise an exception if we try to get the scanner when its not there.""" + with pytest.raises(RuntimeError): + bluetooth.async_get_scanner(hass) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 755b2ec07f8..010989628e1 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -12,6 +12,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + async_get_scanner, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -207,12 +208,14 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True + scanner = async_get_scanner(hass) with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", [MagicMock(address="44:44:33:11:23:45")], - ), patch( - "homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history", + ), patch.object( + scanner, + "history", {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): async_fire_time_changed( @@ -228,8 +231,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", [MagicMock(address="44:44:33:11:23:45")], - ), patch( - "homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history", + ), patch.object( + scanner, + "history", {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): async_fire_time_changed( diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 92339735340..8e566a7ce5a 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -1,22 +1,25 @@ """Tests for the Bluetooth integration.""" -from unittest.mock import MagicMock import bleak -from homeassistant.components.bluetooth import models from homeassistant.components.bluetooth.models import HaBleakScannerWrapper -from homeassistant.components.bluetooth.usage import install_multiple_bleak_catcher +from homeassistant.components.bluetooth.usage import ( + install_multiple_bleak_catcher, + uninstall_multiple_bleak_catcher, +) async def test_multiple_bleak_scanner_instances(hass): - """Test creating multiple zeroconf throws without an integration.""" - assert models.HA_BLEAK_SCANNER is None - mock_scanner = MagicMock() - - install_multiple_bleak_catcher(mock_scanner) + """Test creating multiple BleakScanners without an integration.""" + install_multiple_bleak_catcher() instance = bleak.BleakScanner() assert isinstance(instance, HaBleakScannerWrapper) - assert models.HA_BLEAK_SCANNER is mock_scanner + + uninstall_multiple_bleak_catcher() + + instance = bleak.BleakScanner() + + assert not isinstance(instance, HaBleakScannerWrapper) diff --git a/tests/components/bluetooth_le_tracker/conftest.py b/tests/components/bluetooth_le_tracker/conftest.py index 30b2d5a44fb..9fce8e85ea8 100644 --- a/tests/components/bluetooth_le_tracker/conftest.py +++ b/tests/components/bluetooth_le_tracker/conftest.py @@ -1,7 +1,8 @@ -"""Tests for the bluetooth_le_tracker component.""" +"""Session fixtures.""" + import pytest @pytest.fixture(autouse=True) -def bluetooth_le_tracker_auto_mock_bluetooth(mock_bluetooth): - """Mock the bluetooth integration scanner.""" +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index d82b4109839..f8f8c20dbb2 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -23,7 +23,7 @@ def recorder_url_mock(): yield -async def test_setup(hass, mock_zeroconf, mock_get_source_ip): +async def test_setup(hass, mock_zeroconf, mock_get_source_ip, mock_bluetooth): """Test setup.""" recorder_helper.async_initialize_recorder(hass) assert await async_setup_component(hass, "default_config", {"foo": "bar"}) diff --git a/tests/components/inkbird/conftest.py b/tests/components/inkbird/conftest.py index c44e9f7929a..3450cb933fe 100644 --- a/tests/components/inkbird/conftest.py +++ b/tests/components/inkbird/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(mock_bleak_scanner_start): +def mock_bluetooth(enable_bluetooth): """Auto mock bluetooth.""" diff --git a/tests/components/sensorpush/conftest.py b/tests/components/sensorpush/conftest.py index c6497a4e76d..2a983a7a4ed 100644 --- a/tests/components/sensorpush/conftest.py +++ b/tests/components/sensorpush/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def auto_mock_bleak_scanner_start(mock_bleak_scanner_start): - """Auto mock bleak scanner start.""" +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/conftest.py b/tests/conftest.py index 9ca29c60658..e0f4fb5ab90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -871,6 +871,24 @@ def mock_integration_frame(): yield correct_frame +@pytest.fixture(name="enable_bluetooth") +async def mock_enable_bluetooth( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): + """Fixture to mock starting the bleak scanner.""" + entry = MockConfigEntry(domain="bluetooth") + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +@pytest.fixture(name="mock_bluetooth_adapters") +def mock_bluetooth_adapters(): + """Fixture to mock bluetooth adapters.""" + with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()): + yield + + @pytest.fixture(name="mock_bleak_scanner_start") def mock_bleak_scanner_start(): """Fixture to mock starting the bleak scanner.""" @@ -900,5 +918,5 @@ def mock_bleak_scanner_start(): @pytest.fixture(name="mock_bluetooth") -def mock_bluetooth(mock_bleak_scanner_start): +def mock_bluetooth(mock_bleak_scanner_start, mock_bluetooth_adapters): """Mock out bluetooth from starting.""" From 19db6ecf6d3a7e35c40a0db6e109ccc03d2ef491 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 16:02:15 -0500 Subject: [PATCH 635/820] Add missing inkbird config flow tests (#75630) --- tests/components/inkbird/test_config_flow.py | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index b783562b126..86097aec208 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -92,3 +92,61 @@ async def test_async_step_user_with_found_devices_already_setup(hass): ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" From 5b555066ea428427945aca0824b30e496c653831 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 23 Jul 2022 00:18:22 +0200 Subject: [PATCH 636/820] Add new NextDNS sensors (#74789) --- homeassistant/components/nextdns/sensor.py | 48 ++++++++++++----- tests/components/nextdns/__init__.py | 6 ++- tests/components/nextdns/test_sensor.py | 60 +++++++++++++++++----- 3 files changed, 87 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 174357864b3..168c0be8cd0 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -63,7 +63,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.all_queries, ), NextDnsSensorEntityDescription[AnalyticsStatus]( @@ -73,7 +73,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS queries blocked", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.blocked_queries, ), NextDnsSensorEntityDescription[AnalyticsStatus]( @@ -83,7 +83,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS queries relayed", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.relayed_queries, ), NextDnsSensorEntityDescription[AnalyticsStatus]( @@ -104,7 +104,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS-over-HTTPS queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.doh_queries, ), NextDnsSensorEntityDescription[AnalyticsProtocols]( @@ -115,7 +115,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS-over-TLS queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.dot_queries, ), NextDnsSensorEntityDescription[AnalyticsProtocols]( @@ -126,9 +126,20 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS-over-QUIC queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.doq_queries, ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="tcp_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="TCP Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.tcp_queries, + ), NextDnsSensorEntityDescription[AnalyticsProtocols]( key="udp_queries", coordinator_type=ATTR_PROTOCOLS, @@ -137,7 +148,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="UDP queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.udp_queries, ), NextDnsSensorEntityDescription[AnalyticsProtocols]( @@ -173,6 +184,17 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries_ratio, ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="tcp_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="TCP Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.tcp_queries_ratio, + ), NextDnsSensorEntityDescription[AnalyticsProtocols]( key="udp_queries_ratio", coordinator_type=ATTR_PROTOCOLS, @@ -192,7 +214,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:lock", name="Encrypted queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.encrypted_queries, ), NextDnsSensorEntityDescription[AnalyticsEncryption]( @@ -203,7 +225,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:lock-open", name="Unencrypted queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.unencrypted_queries, ), NextDnsSensorEntityDescription[AnalyticsEncryption]( @@ -225,7 +247,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:ip", name="IPv4 queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.ipv4_queries, ), NextDnsSensorEntityDescription[AnalyticsIpVersions]( @@ -236,7 +258,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:ip", name="IPv6 queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.ipv6_queries, ), NextDnsSensorEntityDescription[AnalyticsIpVersions]( @@ -258,7 +280,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:lock-check", name="DNSSEC validated queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.validated_queries, ), NextDnsSensorEntityDescription[AnalyticsDnssec]( @@ -269,7 +291,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:lock-alert", name="DNSSEC not validated queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.not_validated_queries, ), NextDnsSensorEntityDescription[AnalyticsDnssec]( diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 82c55f56bbb..6eb258ef2c4 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -24,7 +24,11 @@ DNSSEC = AnalyticsDnssec(not_validated_queries=25, validated_queries=75) ENCRYPTION = AnalyticsEncryption(encrypted_queries=60, unencrypted_queries=40) IP_VERSIONS = AnalyticsIpVersions(ipv4_queries=90, ipv6_queries=10) PROTOCOLS = AnalyticsProtocols( - doh_queries=20, doq_queries=10, dot_queries=30, udp_queries=40 + doh_queries=20, + doq_queries=10, + dot_queries=30, + tcp_queries=0, + udp_queries=40, ) SETTINGS = Settings( ai_threat_detection=True, diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index 8a7d13866f3..731f98203ab 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -121,6 +121,20 @@ async def test_sensor(hass): suggested_object_id="fake_profile_ipv6_queries_ratio", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_tcp_queries", + suggested_object_id="fake_profile_tcp_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_tcp_queries_ratio", + suggested_object_id="fake_profile_tcp_queries_ratio", + disabled_by=None, + ) registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, @@ -148,7 +162,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_queries") assert state assert state.state == "100" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_queries") @@ -158,7 +172,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_queries_blocked") assert state assert state.state == "20" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_queries_blocked") @@ -178,7 +192,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_queries_relayed") assert state assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_queries_relayed") @@ -188,7 +202,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_https_queries") assert state assert state.state == "20" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_over_https_queries") @@ -208,7 +222,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_quic_queries") assert state assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries") @@ -228,7 +242,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_tls_queries") assert state assert state.state == "30" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries") @@ -248,7 +262,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dnssec_not_validated_queries") assert state assert state.state == "25" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dnssec_not_validated_queries") @@ -258,7 +272,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") assert state assert state.state == "75" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries") @@ -278,7 +292,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_encrypted_queries") assert state assert state.state == "60" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_encrypted_queries") @@ -288,7 +302,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_unencrypted_queries") assert state assert state.state == "40" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_unencrypted_queries") @@ -308,7 +322,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_ipv4_queries") assert state assert state.state == "90" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_ipv4_queries") @@ -318,7 +332,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_ipv6_queries") assert state assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_ipv6_queries") @@ -335,10 +349,30 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "xyz12_ipv6_queries_ratio" + state = hass.states.get("sensor.fake_profile_tcp_queries") + assert state + assert state.state == "0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_tcp_queries") + assert entry + assert entry.unique_id == "xyz12_tcp_queries" + + state = hass.states.get("sensor.fake_profile_tcp_queries_ratio") + assert state + assert state.state == "0.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_tcp_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_tcp_queries_ratio" + state = hass.states.get("sensor.fake_profile_udp_queries") assert state assert state.state == "40" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_udp_queries") From 88b9a518115a47c51ae4d6143cab08d04b08876c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 17:43:13 -0500 Subject: [PATCH 637/820] Fix inkbird config flow tests to correctly test discovery and user flow (#75638) * Fix inkbird config flow tests to correctly test discovery and user flow * Fix inkbird config flow tests to correctly test discovery and user flow --- tests/components/inkbird/test_config_flow.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index 86097aec208..c1f8b3ef545 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -141,12 +141,24 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): assert result["step_id"] == "bluetooth_confirm" with patch( - "homeassistant.components.sensorpush.async_setup_entry", return_value=True + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "IBS-TH 75BBE1738105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) From 6bb51782fab9fab8d48db1923aac79c0ea7c8e81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 17:45:22 -0500 Subject: [PATCH 638/820] Add missing config flow tests for sensorpush (#75629) * Add missing config flow tests for sensorpush * merge correct commits from integration --- .../components/sensorpush/test_config_flow.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py index 662841af3f1..1c825640603 100644 --- a/tests/components/sensorpush/test_config_flow.py +++ b/tests/components/sensorpush/test_config_flow.py @@ -96,3 +96,75 @@ async def test_async_step_user_with_found_devices_already_setup(hass): ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "HT.w 0CA1" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) From 402e533fefe17b736c2c72f3747dc9edbe8d9eae Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 22 Jul 2022 23:55:06 +0100 Subject: [PATCH 639/820] Initial xiaomi_ble integration (#75618) * Initial xiaomi_ble integration * black * Update homeassistant/components/xiaomi_ble/config_flow.py Co-authored-by: Ernst Klamer * Update homeassistant/components/xiaomi_ble/config_flow.py Co-authored-by: Ernst Klamer * Apply suggestions from code review Co-authored-by: Ernst Klamer * Update tests/components/xiaomi_ble/test_config_flow.py Co-authored-by: Ernst Klamer * Update homeassistant/components/xiaomi_ble/sensor.py Co-authored-by: Ernst Klamer * Update tests/components/xiaomi_ble/test_config_flow.py Co-authored-by: Ernst Klamer * Remove debug code * Need 'proper' MAC when running tests on linux * Need to use proper MAC so validation passes * Add tests for already_in_progress and already_configured * copy test, add session fixture * fix test Co-authored-by: Ernst Klamer Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 + .../components/xiaomi_ble/__init__.py | 56 ++++++ .../components/xiaomi_ble/config_flow.py | 93 ++++++++++ homeassistant/components/xiaomi_ble/const.py | 3 + .../components/xiaomi_ble/manifest.json | 15 ++ homeassistant/components/xiaomi_ble/sensor.py | 149 +++++++++++++++ .../components/xiaomi_ble/strings.json | 21 +++ .../xiaomi_ble/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/xiaomi_ble/__init__.py | 38 ++++ tests/components/xiaomi_ble/conftest.py | 8 + .../components/xiaomi_ble/test_config_flow.py | 174 ++++++++++++++++++ tests/components/xiaomi_ble/test_sensor.py | 50 +++++ 16 files changed, 641 insertions(+) create mode 100644 homeassistant/components/xiaomi_ble/__init__.py create mode 100644 homeassistant/components/xiaomi_ble/config_flow.py create mode 100644 homeassistant/components/xiaomi_ble/const.py create mode 100644 homeassistant/components/xiaomi_ble/manifest.json create mode 100644 homeassistant/components/xiaomi_ble/sensor.py create mode 100644 homeassistant/components/xiaomi_ble/strings.json create mode 100644 homeassistant/components/xiaomi_ble/translations/en.json create mode 100644 tests/components/xiaomi_ble/__init__.py create mode 100644 tests/components/xiaomi_ble/conftest.py create mode 100644 tests/components/xiaomi_ble/test_config_flow.py create mode 100644 tests/components/xiaomi_ble/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 3b38b6e1a5a..073170795a1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1220,6 +1220,8 @@ build.json @home-assistant/supervisor /homeassistant/components/xbox_live/ @MartinHjelmare /homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi /tests/components/xiaomi_aqara/ @danielhiversen @syssi +/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79 +/tests/components/xiaomi_ble/ @Jc2k @Ernst79 /homeassistant/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu /tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu /homeassistant/components/xiaomi_tv/ @simse diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py new file mode 100644 index 00000000000..036ff37a306 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -0,0 +1,56 @@ +"""The Xiaomi Bluetooth integration.""" +from __future__ import annotations + +import logging + +from xiaomi_ble import XiaomiBluetoothDeviceData + +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from .const import DOMAIN +from .sensor import sensor_update_to_bluetooth_data_update + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Xiaomi BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + + data = XiaomiBluetoothDeviceData() + + @callback + def _async_update_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Update data from Xiaomi Bluetooth.""" + return sensor_update_to_bluetooth_data_update(data.update(service_info)) + + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothDataUpdateCoordinator( + hass, + _LOGGER, + update_method=_async_update_data, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py new file mode 100644 index 00000000000..8f478442d6a --- /dev/null +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for Xiaomi Bluetooth integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol +from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Xiaomi Bluetooth.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py new file mode 100644 index 00000000000..9a38c75c05f --- /dev/null +++ b/homeassistant/components/xiaomi_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the Xiaomi Bluetooth integration.""" + +DOMAIN = "xiaomi_ble" diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json new file mode 100644 index 00000000000..cf5ef00777b --- /dev/null +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "xiaomi_ble", + "name": "Xiaomi BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", + "bluetooth": [ + { + "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + } + ], + "requirements": ["xiaomi-ble==0.1.0"], + "dependencies": ["bluetooth"], + "codeowners": ["@Jc2k", "@Ernst79"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py new file mode 100644 index 00000000000..c380490dc8c --- /dev/null +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -0,0 +1,149 @@ +"""Support for xiaomi ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, + PassiveBluetoothEntityKey, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Xiaomi BLE sensors.""" + coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + entry.async_on_unload( + coordinator.async_add_entities_listener( + XiaomiBluetoothSensorEntity, async_add_entities + ) + ) + + +class XiaomiBluetoothSensorEntity( + PassiveBluetoothCoordinatorEntity[ + PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a xiaomi ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.coordinator.entity_data.get(self.entity_key) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 1edd25a7609..175b82dbcfd 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -37,5 +37,9 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ { "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + }, + { + "domain": "xiaomi_ble", + "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ] diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2c7732fee8d..917eca321ea 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -420,6 +420,7 @@ FLOWS = { "ws66i", "xbox", "xiaomi_aqara", + "xiaomi_ble", "xiaomi_miio", "yale_smart_alarm", "yamaha_musiccast", diff --git a/requirements_all.txt b/requirements_all.txt index 15c62c63c2f..9451d3b8495 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2470,6 +2470,9 @@ xbox-webapi==2.0.11 # homeassistant.components.xbox_live xboxapi==2.0.1 +# homeassistant.components.xiaomi_ble +xiaomi-ble==0.1.0 + # homeassistant.components.knx xknx==0.21.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 742d6eb0f3a..3faa25b829f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1655,6 +1655,9 @@ wolf_smartset==0.1.11 # homeassistant.components.xbox xbox-webapi==2.0.11 +# homeassistant.components.xiaomi_ble +xiaomi-ble==0.1.0 + # homeassistant.components.knx xknx==0.21.5 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py new file mode 100644 index 00000000000..6022b15bf51 --- /dev/null +++ b/tests/components/xiaomi_ble/__init__.py @@ -0,0 +1,38 @@ +"""Tests for the SensorPush integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="00:00:00:00:00:00", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfo( + name="LYWSDCGQ", + address="58:2D:34:35:93:21", + rssi=-63, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"P \xaa\x01\xda!\x9354-X\r\x10\x04\xfe\x00H\x02" + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + +MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( + name="MMC_T201_1", + address="00:81:F9:DD:6F:C1", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b'p"\xdb\x00o\xc1o\xdd\xf9\x81\x00\t\x00 \x05\xc6\rc\rQ' + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py new file mode 100644 index 00000000000..9fce8e85ea8 --- /dev/null +++ b/tests/components/xiaomi_ble/conftest.py @@ -0,0 +1,8 @@ +"""Session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py new file mode 100644 index 00000000000..f99cbd21296 --- /dev/null +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -0,0 +1,174 @@ +"""Test the Xiaomi config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + LYWSDCGQ_SERVICE_INFO, + MMC_T201_1_SERVICE_INFO, + NOT_SENSOR_PUSH_SERVICE_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "MMC_T201_1" + assert result2["data"] == {} + assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" + + +async def test_async_step_bluetooth_not_xiaomi(hass): + """Test discovery via bluetooth not xiaomi.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_SENSOR_PUSH_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "58:2D:34:35:93:21"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSDCGQ" + assert result2["data"] == {} + assert result2["result"].unique_id == "58:2D:34:35:93:21" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:35:93:21", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="00:81:F9:DD:6F:C1", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MMC_T201_1_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "00:81:F9:DD:6F:C1"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "MMC_T201_1" + assert result2["data"] == {} + assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py new file mode 100644 index 00000000000..044b2ea3e87 --- /dev/null +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Xiaomi config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import MMC_T201_1_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="00:81:F9:DD:6F:C1", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(MMC_T201_1_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + temp_sensor = hass.states.get("sensor.mmc_t201_1_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "36.8719980616822" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "MMC_T201_1 Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From cb543a21b390319692ede988905dba051936fb61 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 23 Jul 2022 00:58:48 +0200 Subject: [PATCH 640/820] Address NextDNS late review (#75635) * Init instance attributes * Remove condition * Improve typing in tests * Suggested change --- .../components/nextdns/config_flow.py | 6 ++++-- tests/components/nextdns/__init__.py | 7 +------ tests/components/nextdns/test_button.py | 5 +++-- tests/components/nextdns/test_config_flow.py | 15 +++++++------- tests/components/nextdns/test_diagnostics.py | 9 ++++++++- tests/components/nextdns/test_init.py | 20 +++++++++++++------ tests/components/nextdns/test_sensor.py | 5 +++-- .../components/nextdns/test_system_health.py | 10 ++++++++-- 8 files changed, 49 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index c621accfe81..5c9bf04cfc1 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -24,8 +24,8 @@ class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self.nextdns: NextDns - self.api_key: str + self.nextdns: NextDns | None = None + self.api_key: str | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -63,6 +63,8 @@ class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle the profiles step.""" errors: dict[str, str] = {} + assert self.nextdns is not None + if user_input is not None: profile_name = user_input[CONF_PROFILE_NAME] profile_id = self.nextdns.get_profile_id(profile_name) diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 6eb258ef2c4..3b513b811b8 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -57,9 +57,7 @@ SETTINGS = Settings( ) -async def init_integration( - hass: HomeAssistant, add_to_hass: bool = True -) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the NextDNS integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -68,9 +66,6 @@ async def init_integration( data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, ) - if not add_to_hass: - return entry - with patch( "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES ), patch( diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py index 39201a668a4..fabf87f6462 100644 --- a/tests/components/nextdns/test_button.py +++ b/tests/components/nextdns/test_button.py @@ -3,13 +3,14 @@ from unittest.mock import patch from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from . import init_integration -async def test_button(hass): +async def test_button(hass: HomeAssistant) -> None: """Test states of the button.""" registry = er.async_get(hass) @@ -24,7 +25,7 @@ async def test_button(hass): assert entry.unique_id == "xyz12_clear_logs" -async def test_button_press(hass): +async def test_button_press(hass: HomeAssistant) -> None: """Test button press.""" await init_integration(hass) diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index ad17de1c150..5f387fd1f64 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -13,11 +13,12 @@ from homeassistant.components.nextdns.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from . import PROFILES, init_integration -async def test_form_create_entry(hass): +async def test_form_create_entry(hass: HomeAssistant) -> None: """Test that the user step works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -52,7 +53,7 @@ async def test_form_create_entry(hass): @pytest.mark.parametrize( - "error", + "exc,base_error", [ (ApiError("API Error"), "cannot_connect"), (InvalidApiKeyError, "invalid_api_key"), @@ -60,9 +61,10 @@ async def test_form_create_entry(hass): (ValueError, "unknown"), ], ) -async def test_form_errors(hass, error): +async def test_form_errors( + hass: HomeAssistant, exc: Exception, base_error: str +) -> None: """Test we handle errors.""" - exc, base_error = error with patch( "homeassistant.components.nextdns.NextDns.get_profiles", side_effect=exc ): @@ -75,10 +77,9 @@ async def test_form_errors(hass, error): assert result["errors"] == {"base": base_error} -async def test_form_already_configured(hass): +async def test_form_already_configured(hass: HomeAssistant) -> None: """Test that errors are shown when duplicates are added.""" - entry = await init_integration(hass) - entry.add_to_hass(hass) + await init_integration(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index ba4a4d2ccb4..85dbceafff9 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -1,11 +1,18 @@ """Test NextDNS diagnostics.""" +from collections.abc import Awaitable, Callable + +from aiohttp import ClientSession + from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.nextdns import init_integration -async def test_entry_diagnostics(hass, hass_client): +async def test_entry_diagnostics( + hass: HomeAssistant, hass_client: Callable[..., Awaitable[ClientSession]] +) -> None: """Test config entry diagnostics.""" entry = await init_integration(hass) diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py index c16fad4e812..fb9ea74509e 100644 --- a/tests/components/nextdns/test_init.py +++ b/tests/components/nextdns/test_init.py @@ -3,14 +3,17 @@ from unittest.mock import patch from nextdns import ApiError -from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import CONF_API_KEY, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant from . import init_integration +from tests.common import MockConfigEntry -async def test_async_setup_entry(hass): + +async def test_async_setup_entry(hass: HomeAssistant) -> None: """Test a successful setup entry.""" await init_integration(hass) @@ -20,9 +23,14 @@ async def test_async_setup_entry(hass): assert state.state == "20.0" -async def test_config_not_ready(hass): +async def test_config_not_ready(hass: HomeAssistant) -> None: """Test for setup failure if the connection to the service fails.""" - entry = await init_integration(hass, add_to_hass=False) + entry = MockConfigEntry( + domain=DOMAIN, + title="Fake Profile", + unique_id="xyz12", + data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + ) with patch( "homeassistant.components.nextdns.NextDns.get_profiles", @@ -33,7 +41,7 @@ async def test_config_not_ready(hass): assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass): +async def test_unload_entry(hass: HomeAssistant) -> None: """Test successful unload of entry.""" entry = await init_integration(hass) diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index 731f98203ab..a90999a592b 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -19,7 +20,7 @@ from . import DNSSEC, ENCRYPTION, IP_VERSIONS, PROTOCOLS, STATUS, init_integrati from tests.common import async_fire_time_changed -async def test_sensor(hass): +async def test_sensor(hass: HomeAssistant) -> None: """Test states of sensors.""" registry = er.async_get(hass) @@ -390,7 +391,7 @@ async def test_sensor(hass): assert entry.unique_id == "xyz12_udp_queries_ratio" -async def test_availability(hass): +async def test_availability(hass: HomeAssistant) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" registry = er.async_get(hass) diff --git a/tests/components/nextdns/test_system_health.py b/tests/components/nextdns/test_system_health.py index 4fc3a1a7e72..14d447947c1 100644 --- a/tests/components/nextdns/test_system_health.py +++ b/tests/components/nextdns/test_system_health.py @@ -5,12 +5,16 @@ from aiohttp import ClientError from nextdns.const import API_ENDPOINT from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import get_system_health_info +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_nextdns_system_health(hass, aioclient_mock): +async def test_nextdns_system_health( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test NextDNS system health.""" aioclient_mock.get(API_ENDPOINT, text="") hass.config.components.add(DOMAIN) @@ -25,7 +29,9 @@ async def test_nextdns_system_health(hass, aioclient_mock): assert info == {"can_reach_server": "ok"} -async def test_nextdns_system_health_fail(hass, aioclient_mock): +async def test_nextdns_system_health_fail( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test NextDNS system health.""" aioclient_mock.get(API_ENDPOINT, exc=ClientError) hass.config.components.add(DOMAIN) From 326e05dcf1fdc524abdde92ccae98612bdfe15bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 18:12:08 -0500 Subject: [PATCH 641/820] Fix async_get_scanner to return the correct bluetooth scanner (#75637) --- .../components/bluetooth/__init__.py | 15 +-- tests/components/bluetooth/__init__.py | 7 ++ tests/components/bluetooth/test_init.py | 95 ++++++++++--------- .../test_passive_update_coordinator.py | 5 +- 4 files changed, 70 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 3c7f2393257..a50be7f4ace 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -35,7 +35,7 @@ from homeassistant.loader import ( from . import models from .const import DOMAIN -from .models import HaBleakScanner +from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher _LOGGER = logging.getLogger(__name__) @@ -117,8 +117,12 @@ BluetoothCallback = Callable[ @hass_callback -def async_get_scanner(hass: HomeAssistant) -> HaBleakScanner: - """Return a HaBleakScanner.""" +def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: + """Return a HaBleakScannerWrapper. + + This is a wrapper around our BleakScanner singleton that allows + multiple integrations to share the same BleakScanner. + """ if DOMAIN not in hass.data: raise RuntimeError("Bluetooth integration not loaded") manager: BluetoothManager = hass.data[DOMAIN] @@ -320,10 +324,9 @@ class BluetoothManager: models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() @hass_callback - def async_get_scanner(self) -> HaBleakScanner: + def async_get_scanner(self) -> HaBleakScannerWrapper: """Get the scanner.""" - assert self.scanner is not None - return self.scanner + return HaBleakScannerWrapper() async def async_start(self, scanning_mode: BluetoothScanningMode) -> None: """Set up BT Discovery.""" diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 6bf53afcd1e..3dc80d55590 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -1 +1,8 @@ """Tests for the Bluetooth integration.""" + +from homeassistant.components.bluetooth import models + + +def _get_underlying_scanner(): + """Return the underlying scanner that has been wrapped.""" + return models.HA_BLEAK_SCANNER diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 8aef5f3ddbb..5d932c56349 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -12,7 +12,6 @@ from homeassistant.components.bluetooth import ( UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, - async_get_scanner, async_track_unavailable, models, ) @@ -21,6 +20,8 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from . import _get_underlying_scanner + from tests.common import MockConfigEntry, async_fire_time_changed @@ -135,7 +136,7 @@ async def test_discovery_match_by_service_uuid( wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - async_get_scanner(hass)._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -145,7 +146,7 @@ async def test_discovery_match_by_service_uuid( local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -172,7 +173,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - async_get_scanner(hass)._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -180,7 +181,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -219,7 +220,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( manufacturer_data={76: b"\x06\x02\x03\x99"}, ) - async_get_scanner(hass)._callback(hkc_device, hkc_adv) + _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -227,7 +228,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( mock_config_flow.reset_mock() # 2nd discovery should not generate another flow - async_get_scanner(hass)._callback(hkc_device, hkc_adv) + _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -238,7 +239,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} ) - async_get_scanner(hass)._callback(not_hkc_device, not_hkc_adv) + _get_underlying_scanner()._callback(not_hkc_device, not_hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -247,7 +248,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} ) - async_get_scanner(hass)._callback(not_apple_device, not_apple_adv) + _get_underlying_scanner()._callback(not_apple_device, not_apple_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -279,10 +280,10 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - async_get_scanner(hass)._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) wrong_device_went_unavailable = False switchbot_device_went_unavailable = False @@ -316,8 +317,8 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): assert wrong_device_went_unavailable is True # See the devices again - async_get_scanner(hass)._callback(wrong_device, wrong_adv) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) # Cancel the callbacks wrong_device_unavailable_cancel() switchbot_device_unavailable_cancel() @@ -382,25 +383,25 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() assert len(callbacks) == 3 @@ -467,25 +468,25 @@ async def test_register_callback_by_address( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() # Now register again with a callback that fails to @@ -549,15 +550,15 @@ async def test_wrapped_instance_with_filter( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) mock_discovered = [MagicMock()] - type(async_get_scanner(hass)).discovered_devices = mock_discovered - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + type(_get_underlying_scanner()).discovered_devices = mock_discovered + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() discovered = await scanner.discover(timeout=0) @@ -572,17 +573,17 @@ async def test_wrapped_instance_with_filter( # We should get a reply from the history when we register again assert len(detected) == 3 - type(async_get_scanner(hass)).discovered_devices = [] + type(_get_underlying_scanner()).discovered_devices = [] discovered = await scanner.discover(timeout=0) assert len(discovered) == 0 assert discovered == [] - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) assert len(detected) == 4 # The filter we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 4 @@ -620,22 +621,22 @@ async def test_wrapped_instance_with_service_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 2 @@ -673,15 +674,15 @@ async def test_wrapped_instance_with_broken_callbacks( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 1 @@ -719,23 +720,23 @@ async def test_wrapped_instance_changes_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 2 @@ -772,23 +773,23 @@ async def test_wrapped_instance_changes_filters( empty_device = BLEDevice("11:22:33:44:55:62", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) - type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 2 @@ -807,7 +808,7 @@ async def test_wrapped_instance_unsupported_filter( with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={ @@ -845,7 +846,7 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert ( @@ -935,3 +936,9 @@ async def test_raising_runtime_error_when_no_bluetooth(hass): """Test we raise an exception if we try to get the scanner when its not there.""" with pytest.raises(RuntimeError): bluetooth.async_get_scanner(hass) + + +async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_bluetooth): + """Test getting the scanner returns the wrapped instance.""" + scanner = bluetooth.async_get_scanner(hass) + assert isinstance(scanner, models.HaBleakScannerWrapper) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 010989628e1..48f2e8edf06 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -12,7 +12,6 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, - async_get_scanner, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -27,6 +26,8 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from . import _get_underlying_scanner + from tests.common import MockEntityPlatform, async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -208,7 +209,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - scanner = async_get_scanner(hass) + scanner = _get_underlying_scanner() with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", From 8e8612447021848bc6c5f69b07255fdcd17a2b90 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 23 Jul 2022 00:25:17 +0000 Subject: [PATCH 642/820] [ci skip] Translation update --- .../components/bluetooth/translations/de.json | 16 ++++++++++++++ .../components/bluetooth/translations/el.json | 16 ++++++++++++++ .../components/bluetooth/translations/id.json | 16 ++++++++++++++ .../components/bluetooth/translations/it.json | 16 ++++++++++++++ .../components/bluetooth/translations/ja.json | 16 ++++++++++++++ .../components/bluetooth/translations/pl.json | 16 ++++++++++++++ .../bluetooth/translations/pt-BR.json | 16 ++++++++++++++ .../bluetooth/translations/zh-Hant.json | 16 ++++++++++++++ .../components/demo/translations/el.json | 21 +++++++++++++++++++ .../components/demo/translations/id.json | 21 +++++++++++++++++++ .../components/demo/translations/it.json | 5 +++++ .../components/demo/translations/ja.json | 21 +++++++++++++++++++ .../components/google/translations/el.json | 3 ++- .../homekit_controller/translations/it.json | 2 +- .../components/inkbird/translations/de.json | 21 +++++++++++++++++++ .../components/inkbird/translations/el.json | 21 +++++++++++++++++++ .../components/inkbird/translations/id.json | 21 +++++++++++++++++++ .../components/inkbird/translations/ja.json | 21 +++++++++++++++++++ .../components/inkbird/translations/pl.json | 21 +++++++++++++++++++ .../inkbird/translations/pt-BR.json | 21 +++++++++++++++++++ .../inkbird/translations/zh-Hant.json | 21 +++++++++++++++++++ .../components/lifx/translations/el.json | 20 ++++++++++++++++++ .../components/lifx/translations/id.json | 20 ++++++++++++++++++ .../components/lifx/translations/ja.json | 20 ++++++++++++++++++ .../components/plugwise/translations/el.json | 3 ++- .../components/plugwise/translations/id.json | 3 ++- .../components/plugwise/translations/ja.json | 3 ++- .../sensorpush/translations/de.json | 21 +++++++++++++++++++ .../sensorpush/translations/el.json | 21 +++++++++++++++++++ .../sensorpush/translations/id.json | 21 +++++++++++++++++++ .../sensorpush/translations/ja.json | 21 +++++++++++++++++++ .../sensorpush/translations/pl.json | 21 +++++++++++++++++++ .../sensorpush/translations/pt-BR.json | 21 +++++++++++++++++++ .../sensorpush/translations/zh-Hant.json | 21 +++++++++++++++++++ .../components/uscis/translations/de.json | 8 +++++++ .../components/uscis/translations/el.json | 8 +++++++ .../components/uscis/translations/id.json | 8 +++++++ .../components/uscis/translations/it.json | 8 +++++++ .../components/uscis/translations/ja.json | 7 +++++++ .../components/verisure/translations/el.json | 15 ++++++++++++- .../components/withings/translations/it.json | 3 +++ .../components/zha/translations/de.json | 1 + .../components/zha/translations/el.json | 1 + .../components/zha/translations/id.json | 1 + .../components/zha/translations/pl.json | 1 + .../components/zha/translations/zh-Hant.json | 1 + 46 files changed, 620 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/bluetooth/translations/de.json create mode 100644 homeassistant/components/bluetooth/translations/el.json create mode 100644 homeassistant/components/bluetooth/translations/id.json create mode 100644 homeassistant/components/bluetooth/translations/it.json create mode 100644 homeassistant/components/bluetooth/translations/ja.json create mode 100644 homeassistant/components/bluetooth/translations/pl.json create mode 100644 homeassistant/components/bluetooth/translations/pt-BR.json create mode 100644 homeassistant/components/bluetooth/translations/zh-Hant.json create mode 100644 homeassistant/components/inkbird/translations/de.json create mode 100644 homeassistant/components/inkbird/translations/el.json create mode 100644 homeassistant/components/inkbird/translations/id.json create mode 100644 homeassistant/components/inkbird/translations/ja.json create mode 100644 homeassistant/components/inkbird/translations/pl.json create mode 100644 homeassistant/components/inkbird/translations/pt-BR.json create mode 100644 homeassistant/components/inkbird/translations/zh-Hant.json create mode 100644 homeassistant/components/sensorpush/translations/de.json create mode 100644 homeassistant/components/sensorpush/translations/el.json create mode 100644 homeassistant/components/sensorpush/translations/id.json create mode 100644 homeassistant/components/sensorpush/translations/ja.json create mode 100644 homeassistant/components/sensorpush/translations/pl.json create mode 100644 homeassistant/components/sensorpush/translations/pt-BR.json create mode 100644 homeassistant/components/sensorpush/translations/zh-Hant.json create mode 100644 homeassistant/components/uscis/translations/de.json create mode 100644 homeassistant/components/uscis/translations/el.json create mode 100644 homeassistant/components/uscis/translations/id.json create mode 100644 homeassistant/components/uscis/translations/it.json create mode 100644 homeassistant/components/uscis/translations/ja.json diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json new file mode 100644 index 00000000000..1c677be6828 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json new file mode 100644 index 00000000000..05198f0929a --- /dev/null +++ b/homeassistant/components/bluetooth/translations/el.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json new file mode 100644 index 00000000000..12653696241 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json new file mode 100644 index 00000000000..12d79b3efff --- /dev/null +++ b/homeassistant/components/bluetooth/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json new file mode 100644 index 00000000000..f42ea56e2c8 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json new file mode 100644 index 00000000000..4b93cce9f7e --- /dev/null +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json new file mode 100644 index 00000000000..9cafa844652 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json new file mode 100644 index 00000000000..3f92640469e --- /dev/null +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/el.json b/homeassistant/components/demo/translations/el.json index 34afbe9df01..c4c539034bd 100644 --- a/homeassistant/components/demo/translations/el.json +++ b/homeassistant/components/demo/translations/el.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u03a0\u03b9\u03ad\u03c3\u03c4\u03b5 OK \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9.", + "title": "\u03a4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9" + } + } + }, + "title": "\u03a4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03b4\u03b5\u03b9\u03bf \u03ba\u03b1\u03b9 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9." + }, + "transmogrifier_deprecated": { + "description": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf transmogrifier \u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bb\u03cc\u03b3\u03c9 \u03c4\u03b7\u03c2 \u03ad\u03bb\u03bb\u03b5\u03b9\u03c8\u03b7\u03c2 \u03c4\u03bf\u03c0\u03b9\u03ba\u03bf\u03cd \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c0\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf\u03c2 \u03c3\u03c4\u03bf \u03bd\u03ad\u03bf API", + "title": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03c4\u03bf\u03c5 transmogrifier \u03b1\u03c0\u03bf\u03c3\u03cd\u03c1\u03b5\u03c4\u03b1\u03b9" + }, + "unfixable_problem": { + "description": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03c0\u03bf\u03c4\u03ad \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03bb\u03b5\u03af\u03c8\u03b5\u03b9.", + "title": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/id.json b/homeassistant/components/demo/translations/id.json index 8adbeb3e3c4..e8f827e2b86 100644 --- a/homeassistant/components/demo/translations/id.json +++ b/homeassistant/components/demo/translations/id.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tekan Oke saat cairan blinker telah diisi ulang", + "title": "Cairan blinker perlu diisi ulang" + } + } + }, + "title": "Cairan blinker kosong dan perlu diisi ulang" + }, + "transmogrifier_deprecated": { + "description": "Komponen transmogrifier tidak akan digunakan lagi karena tidak tersedianya kontrol lokal yang tersedia di API baru", + "title": "Komponen transmogrifier tidak akan digunakan lagi" + }, + "unfixable_problem": { + "description": "Masalah ini akan terus terjadi.", + "title": "Masalah ini tidak dapat diatasi." + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 1c94dea053f..7fc00caf26e 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -1,4 +1,9 @@ { + "issues": { + "unfixable_problem": { + "title": "Questo non \u00e8 un problema risolvibile" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index e543a83bfe5..30467e3df5b 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u306e\u88dc\u5145\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001OK\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u3092\u88dc\u5145\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + } + } + }, + "title": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u304c\u7a7a\u306b\u306a\u3063\u305f\u306e\u3067\u3001\u88dc\u5145\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + }, + "transmogrifier_deprecated": { + "description": "\u65b0\u3057\u3044API\u3067\u5229\u7528\u3067\u304d\u308b\u30ed\u30fc\u30ab\u30eb\u5236\u5fa1\u304c\u306a\u3044\u305f\u3081\u3001transmogrifier\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306f\u3001\u975e\u63a8\u5968\u306b\u306a\u308a\u307e\u3057\u305f", + "title": "transmogrifier\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306f\u3001\u975e\u63a8\u5968\u306b\u306a\u308a\u307e\u3057\u305f" + }, + "unfixable_problem": { + "description": "\u3053\u306e\u554f\u984c\u306f\u6c7a\u3057\u3066\u3042\u304d\u3089\u3081\u308b\u3064\u3082\u308a\u306f\u3042\u308a\u307e\u305b\u3093\u3002", + "title": "\u3053\u308c\u306f\u3001\u4fee\u6b63\u53ef\u80fd\u306a\u554f\u984c\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index 65cc5a0038d..21e5580da3d 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -11,7 +11,8 @@ "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd.", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "create_entry": { "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index 947e03374b4..5c71b00a801 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Consenti l'associazione con codici di installazione non sicuri.", "pairing_code": "Codice di abbinamento" }, - "description": "Il controller HomeKit comunica con {name} sulla rete locale utilizzando una connessione cifrata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", + "description": "Il controller HomeKit comunica con {name} ({category}) sulla rete locale utilizzando una connessione cifrata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", "title": "Associazione con un dispositivo tramite il Protocollo degli Accessori HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/inkbird/translations/de.json b/homeassistant/components/inkbird/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/inkbird/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/el.json b/homeassistant/components/inkbird/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/inkbird/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/id.json b/homeassistant/components/inkbird/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/inkbird/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/ja.json b/homeassistant/components/inkbird/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/inkbird/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/pl.json b/homeassistant/components/inkbird/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/inkbird/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/pt-BR.json b/homeassistant/components/inkbird/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/inkbird/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/zh-Hant.json b/homeassistant/components/inkbird/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/inkbird/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/el.json b/homeassistant/components/lifx/translations/el.json index 5a1d7707f55..4ebea49190d 100644 --- a/homeassistant/components/lifx/translations/el.json +++ b/homeassistant/components/lifx/translations/el.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf LIFX;" + }, + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {label} ({host}) {serial};" + }, + "pick_device": { + "data": { + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0391\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b5\u03bd\u03cc, \u03b7 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." } } } diff --git a/homeassistant/components/lifx/translations/id.json b/homeassistant/components/lifx/translations/id.json index 03b2b387c6f..8781581bb0f 100644 --- a/homeassistant/components/lifx/translations/id.json +++ b/homeassistant/components/lifx/translations/id.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Ingin menyiapkan LIFX?" + }, + "discovery_confirm": { + "description": "Ingin menyiapkan {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Perangkat" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Jika host dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat." } } } diff --git a/homeassistant/components/lifx/translations/ja.json b/homeassistant/components/lifx/translations/ja.json index 1945c3112f0..c3b144222a4 100644 --- a/homeassistant/components/lifx/translations/ja.json +++ b/homeassistant/components/lifx/translations/ja.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "LIFX\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" + }, + "discovery_confirm": { + "description": "{label} ({host}) {serial} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?" + }, + "pick_device": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9" + } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u767d\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index caee7bb5a88..18a50e86b66 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "anna_with_adam": "\u03a4\u03cc\u03c3\u03bf \u03b7 \u0386\u03bd\u03bd\u03b1 \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03bf \u0391\u03b4\u03ac\u03bc \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd. \u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u0391\u03b4\u03ac\u03bc \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u0386\u03bd\u03bd\u03b1 \u03c3\u03b1\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index 22c871e4f83..daa2824df27 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "anna_with_adam": "Baik Anna dan Adam terdeteksi. Tambahkan Adam, bukan Anna" }, "error": { "cannot_connect": "Gagal terhubung", diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index 0103774c138..15eb388d2c8 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "anna_with_adam": "\u30a2\u30f3\u30ca\u3068\u30a2\u30c0\u30e0\u306e\u4e21\u65b9\u3092\u691c\u51fa\u3057\u307e\u3057\u305f\u3002\u30a2\u30f3\u30ca\u306e\u4ee3\u308f\u308a\u306b\u30a2\u30c0\u30e0\u3092\u8ffd\u52a0" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/sensorpush/translations/de.json b/homeassistant/components/sensorpush/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/el.json b/homeassistant/components/sensorpush/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/id.json b/homeassistant/components/sensorpush/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/ja.json b/homeassistant/components/sensorpush/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/pl.json b/homeassistant/components/sensorpush/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/pt-BR.json b/homeassistant/components/sensorpush/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/zh-Hant.json b/homeassistant/components/sensorpush/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/sensorpush/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/de.json b/homeassistant/components/uscis/translations/de.json new file mode 100644 index 00000000000..273a340d892 --- /dev/null +++ b/homeassistant/components/uscis/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die Integration der U.S. Citizenship and Immigration Services (USCIS) wird aus Home Assistant entfernt und steht ab Home Assistant 2022.10 nicht mehr zur Verf\u00fcgung.\n\nDie Integration wird entfernt, weil sie auf Webscraping beruht, was nicht erlaubt ist.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die USCIS-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/el.json b/homeassistant/components/uscis/translations/el.json new file mode 100644 index 00000000000..d89ae50773a --- /dev/null +++ b/homeassistant/components/uscis/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03a5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u0399\u03b8\u03b1\u03b3\u03ad\u03bd\u03b5\u03b9\u03b1\u03c2 \u03ba\u03b1\u03b9 \u039c\u03b5\u03c4\u03b1\u03bd\u03ac\u03c3\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03c9\u03bd \u0397\u03a0\u0391 (USCIS) \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03c0\u03c1\u03bf\u03c2 \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10.\n\n\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 webscraping, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9.\n\n\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 USCIS \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/id.json b/homeassistant/components/uscis/translations/id.json new file mode 100644 index 00000000000..37e7278a916 --- /dev/null +++ b/homeassistant/components/uscis/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasi Layanan Kewarganegaraan dan Imigrasi AS (USCIS) sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nIntegrasi ini dalam proses penghapusan, karena bergantung pada proses webscraping, yang tidak diizinkan.\n\nHapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi USCIS dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/it.json b/homeassistant/components/uscis/translations/it.json new file mode 100644 index 00000000000..1e23b69eee5 --- /dev/null +++ b/homeassistant/components/uscis/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione U.S. Citizenship and Immigration Services (USCIS) \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\n L'integrazione verr\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione USCIS verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ja.json b/homeassistant/components/uscis/translations/ja.json new file mode 100644 index 00000000000..b5abb7e0825 --- /dev/null +++ b/homeassistant/components/uscis/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "USCIS\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/el.json b/homeassistant/components/verisure/translations/el.json index 7d46b4ed96b..fc9e4cc52df 100644 --- a/homeassistant/components/verisure/translations/el.json +++ b/homeassistant/components/verisure/translations/el.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unknown_mfa": "\u0395\u03bc\u03c6\u03b1\u03bd\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "\u03a4\u03bf Home Assistant \u03b2\u03c1\u03ae\u03ba\u03b5 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ad\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 Verisure \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 My Pages. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf Home Assistant." }, + "mfa": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2", + "description": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c3\u03b5 2 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03b7 Verisure." + } + }, "reauth_confirm": { "data": { "description": "\u0395\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" } }, + "reauth_mfa": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2", + "description": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c3\u03b5 2 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03b7 Verisure." + } + }, "user": { "data": { "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Verisure My Pages.", diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 079acb0b503..412b75fc1b5 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -27,6 +27,9 @@ "reauth": { "description": "Il profilo \"{profile}\" deve essere autenticato nuovamente per continuare a ricevere i dati Withings.", "title": "Autentica nuovamente l'integrazione" + }, + "reauth_confirm": { + "description": "Il profilo \" {profile} \" deve essere riautenticato per poter continuare a ricevere i dati Withings." } } } diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 88638c6c696..9b8acdf5b87 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", + "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", "title": "Globale Optionen" } }, diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index e0fb76cd6cb..e0e063df426 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "\u0398\u03b5\u03c9\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03c9\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "default_light_transition": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "enable_identify_on_join": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03c6\u03ad \u03b1\u03bd\u03b1\u03b3\u03bd\u03ce\u03c1\u03b9\u03c3\u03b7\u03c2 \u03cc\u03c4\u03b1\u03bd \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "enhanced_light_transition": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b2\u03b5\u03bb\u03c4\u03b9\u03c9\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2/\u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", "title": "\u039a\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03ad\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" } }, diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 5a10e3d01af..63eee2f376b 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Anggap perangkat bertenaga listrik sebagai tidak tersedia setelah (detik)", "default_light_transition": "Waktu transisi lampu default (detik)", "enable_identify_on_join": "Aktifkan efek identifikasi saat perangkat bergabung dengan jaringan", + "enhanced_light_transition": "Aktifkan versi canggih untuk transisi warna/suhu cahaya dari keadaan tidak aktif", "title": "Opsi Global" } }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 6c67c6aea93..0b8e90f5ac0 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z gniazdka za niedost\u0119pne po (sekundach)", "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", + "enhanced_light_transition": "W\u0142\u0105cz ulepszone przej\u015bcie koloru \u015bwiat\u0142a/temperatury ze stanu wy\u0142\u0105czenia", "title": "Opcje og\u00f3lne" } }, diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 9bf7d3c9208..9505da31e80 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "\u5c07\u4e3b\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", + "enhanced_light_transition": "\u958b\u555f\u7531\u95dc\u9589\u72c0\u614b\u589e\u5f37\u5149\u8272/\u8272\u6eab\u8f49\u63db", "title": "Global \u9078\u9805" } }, From edaebcd85d0e62739b883d65eb2848072094dbae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 02:47:02 -0500 Subject: [PATCH 643/820] Pass in the bleak scanner instance to HKC (#75636) --- .../components/homekit_controller/manifest.json | 4 ++-- homeassistant/components/homekit_controller/utils.py | 9 +++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/conftest.py | 5 +++++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index c784bad0d06..47472ba66bf 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,10 +3,10 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.9"], + "requirements": ["aiohomekit==1.1.10"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], - "after_dependencies": ["zeroconf"], + "dependencies": ["bluetooth", "zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"] diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 892040d535f..d7780029331 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -3,7 +3,7 @@ from typing import cast from aiohomekit import Controller -from homeassistant.components import zeroconf +from homeassistant.components import bluetooth, zeroconf from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant @@ -28,7 +28,12 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: if existing := hass.data.get(CONTROLLER): return cast(Controller, existing) - controller = Controller(async_zeroconf_instance=async_zeroconf_instance) + bleak_scanner_instance = bluetooth.async_get_scanner(hass) + + controller = Controller( + async_zeroconf_instance=async_zeroconf_instance, + bleak_scanner_instance=bleak_scanner_instance, + ) hass.data[CONTROLLER] = controller diff --git a/requirements_all.txt b/requirements_all.txt index 9451d3b8495..293eff63383 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.9 +aiohomekit==1.1.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3faa25b829f..c4fb04b7cda 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.9 +aiohomekit==1.1.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 81688f88a4b..043213ec159 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -37,3 +37,8 @@ def controller(hass): @pytest.fixture(autouse=True) def hk_mock_async_zeroconf(mock_async_zeroconf): """Auto mock zeroconf.""" + + +@pytest.fixture(autouse=True) +def auto_mock_bluetooth(mock_bluetooth): + """Auto mock bluetooth.""" From b60a59270c4d906f277b81572a077c6d5bc7d1d6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 23 Jul 2022 09:40:56 +0100 Subject: [PATCH 644/820] Add support for rest of sensors for HHCCJCY01 (#75646) --- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 24 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/__init__.py | 15 ++++ tests/components/xiaomi_ble/test_sensor.py | 84 ++++++++++++++++++- 6 files changed, 124 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index cf5ef00777b..17e22accd6d 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.1.0"], + "requirements": ["xiaomi-ble==0.2.0"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index c380490dc8c..f485ccb8eb6 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -22,6 +22,8 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + CONDUCTIVITY, + LIGHT_LUX, PERCENTAGE, PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -46,6 +48,12 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + (DeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( + key=f"{DeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", device_class=SensorDeviceClass.PRESSURE, @@ -68,6 +76,20 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + # Used for e.g. moisture sensor on HHCCJCY01 + (None, Units.PERCENTAGE): SensorEntityDescription( + key=str(Units.PERCENTAGE), + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for e.g. conductivity sensor on HHCCJCY01 + (None, Units.CONDUCTIVITY): SensorEntityDescription( + key=str(Units.CONDUCTIVITY), + device_class=None, + native_unit_of_measurement=CONDUCTIVITY, + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -106,7 +128,7 @@ def sensor_update_to_bluetooth_data_update( (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() - if description.device_class and description.native_unit_of_measurement + if description.native_unit_of_measurement }, entity_data={ _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value diff --git a/requirements_all.txt b/requirements_all.txt index 293eff63383..80ca6871b48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2471,7 +2471,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.1.0 +xiaomi-ble==0.2.0 # homeassistant.components.knx xknx==0.21.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4fb04b7cda..6d0e6f54715 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1656,7 +1656,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.1.0 +xiaomi-ble==0.2.0 # homeassistant.components.knx xknx==0.21.5 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 6022b15bf51..80ec2f19989 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -36,3 +36,18 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", ) + + +def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: + """Make a dummy advertisement.""" + return BluetoothServiceInfo( + name="Test Device", + address=address, + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": payload, + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", + ) diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 044b2ea3e87..44b29ff1051 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT -from . import MMC_T201_1_SERVICE_INFO +from . import MMC_T201_1_SERVICE_INFO, make_advertisement from tests.common import MockConfigEntry @@ -48,3 +48,85 @@ async def test_sensors(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_xiaomi_HHCCJCY01(hass): + """This device has multiple advertisements before all sensors are visible. Test that this works.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00fz>j\x8d|\xc4\r\x07\x10\x03\x00\x00\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00hz>j\x8d|\xc4\r\t\x10\x02W\x02" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00Gz>j\x8d|\xc4\r\x08\x10\x01@" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x04\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + illum_sensor = hass.states.get("sensor.test_device_illuminance") + illum_sensor_attr = illum_sensor.attributes + assert illum_sensor.state == "0" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Illuminance" + assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + cond_sensor = hass.states.get("sensor.test_device_conductivity") + cond_sensor_attribtes = cond_sensor.attributes + assert cond_sensor.state == "599" + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Conductivity" + assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + moist_sensor = hass.states.get("sensor.test_device_moisture") + moist_sensor_attribtes = moist_sensor.attributes + assert moist_sensor.state == "64" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Moisture" + assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "24.4" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 5e10716dd84fe5cb227368e7f0e42b0b2c3a66ea Mon Sep 17 00:00:00 2001 From: Jelte Zeilstra Date: Sat, 23 Jul 2022 12:42:43 +0200 Subject: [PATCH 645/820] Do not access hass.data in unifi test (#75348) * Do not access hass.data in test * Process review feedback --- tests/components/unifi/test_update.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index 7bbeb65497e..677491319a1 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -5,7 +5,7 @@ from aiounifi.controller import MESSAGE_DEVICE from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING from yarl import URL -from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN +from homeassistant.components.unifi.const import CONF_SITE_ID from homeassistant.components.update import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, @@ -19,6 +19,7 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + CONF_HOST, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -166,8 +167,7 @@ async def test_install(hass, aioclient_mock): device_state = hass.states.get("update.device_1") assert device_state.state == STATE_ON - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - url = f"https://{controller.host}:1234/api/s/{controller.site}/cmd/devmgr" + url = f"https://{config_entry.data[CONF_HOST]}:1234/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr" aioclient_mock.clear_requests() aioclient_mock.post(url) From 5c234a3504fd0d399c63b045dbe86fbe2d26f292 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Jul 2022 07:24:34 -0400 Subject: [PATCH 646/820] Use CO Device Class Instead of Gas in zwave_js (#75649) Use CO Device Class Instead of Gas Switches the carbon monoxide sensor from `Gas` to `CO` --- homeassistant/components/zwave_js/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index f6480689910..7fa20b2b1f5 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -90,7 +90,7 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = # NotificationType 2: Carbon Monoxide - State Id's 1 and 2 key=NOTIFICATION_CARBON_MONOOXIDE, states=("1", "2"), - device_class=BinarySensorDeviceClass.GAS, + device_class=BinarySensorDeviceClass.CO, ), NotificationZWaveJSEntityDescription( # NotificationType 2: Carbon Monoxide - All other State Id's From b71e3397fd69c6ea63eaa6cd918bcd4b12fa424e Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 23 Jul 2022 23:40:45 +0800 Subject: [PATCH 647/820] Add error message for duplicate stream recordings (#75654) --- homeassistant/components/stream/recorder.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index b33a5fbbf84..42b98946c15 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -163,4 +163,10 @@ class RecorderOutput(StreamOutput): _LOGGER.error("Recording failed to capture anything") else: output.close() - os.rename(self.video_path + ".tmp", self.video_path) + try: + os.rename(self.video_path + ".tmp", self.video_path) + except FileNotFoundError: + _LOGGER.error( + "Error writing to '%s'. There are likely multiple recordings writing to the same file", + self.video_path, + ) From 8d6247446bac4060d47ded1761b8a44e2b61bb55 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Jul 2022 17:47:12 +0200 Subject: [PATCH 648/820] Automatically set up Bluetooth during onboarding (#75658) --- .../components/bluetooth/config_flow.py | 3 ++- .../components/bluetooth/test_config_flow.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 2193170810f..8fe01be769d 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from homeassistant.components import onboarding from homeassistant.config_entries import ConfigFlow from homeassistant.data_entry_flow import FlowResult @@ -27,7 +28,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="already_configured") - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry(title=DEFAULT_NAME, data={}) return self.async_show_form(step_id="enable_bluetooth") diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 550f5a583d7..5c6199b9bf0 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -64,6 +64,27 @@ async def test_async_step_integration_discovery(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_async_step_integration_discovery_during_onboarding(hass): + """Test setting up from integration discovery during onboarding.""" + + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Bluetooth" + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + async def test_async_step_integration_discovery_already_exists(hass): """Test setting up from integration discovery when an entry already exists.""" entry = MockConfigEntry(domain=DOMAIN) From 759add51841450b89f5742414345bf1144dcf195 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 11:14:06 -0500 Subject: [PATCH 649/820] Add state class to HKC sensors (#75662) --- .../components/homekit_controller/sensor.py | 20 +++++++++---------- .../specific_devices/test_aqara_switch.py | 2 ++ .../specific_devices/test_arlo_baby.py | 3 +++ .../specific_devices/test_eve_degree.py | 3 +++ .../specific_devices/test_hue_bridge.py | 2 ++ .../test_ryse_smart_bridge.py | 7 +++++++ .../specific_devices/test_velux_gateway.py | 4 ++++ .../homekit_controller/test_sensor.py | 5 +++-- 8 files changed, 34 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index a3a722fea67..cddcbc59cde 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -34,8 +34,6 @@ from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity from .connection import HKDevice from .utils import folded_name -CO2_ICON = "mdi:molecule-co2" - @dataclass class HomeKitSensorEntityDescription(SensorEntityDescription): @@ -200,9 +198,11 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { } -class HomeKitSensor(HomeKitEntity): +class HomeKitSensor(HomeKitEntity, SensorEntity): """Representation of a HomeKit sensor.""" + _attr_state_class = SensorStateClass.MEASUREMENT + @property def name(self) -> str | None: """Return the name of the device.""" @@ -217,7 +217,7 @@ class HomeKitSensor(HomeKitEntity): return full_name -class HomeKitHumiditySensor(HomeKitSensor, SensorEntity): +class HomeKitHumiditySensor(HomeKitSensor): """Representation of a Homekit humidity sensor.""" _attr_device_class = SensorDeviceClass.HUMIDITY @@ -238,7 +238,7 @@ class HomeKitHumiditySensor(HomeKitSensor, SensorEntity): return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) -class HomeKitTemperatureSensor(HomeKitSensor, SensorEntity): +class HomeKitTemperatureSensor(HomeKitSensor): """Representation of a Homekit temperature sensor.""" _attr_device_class = SensorDeviceClass.TEMPERATURE @@ -259,7 +259,7 @@ class HomeKitTemperatureSensor(HomeKitSensor, SensorEntity): return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) -class HomeKitLightSensor(HomeKitSensor, SensorEntity): +class HomeKitLightSensor(HomeKitSensor): """Representation of a Homekit light level sensor.""" _attr_device_class = SensorDeviceClass.ILLUMINANCE @@ -280,10 +280,10 @@ class HomeKitLightSensor(HomeKitSensor, SensorEntity): return self.service.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT) -class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): +class HomeKitCarbonDioxideSensor(HomeKitSensor): """Representation of a Homekit Carbon Dioxide sensor.""" - _attr_icon = CO2_ICON + _attr_device_class = SensorDeviceClass.CO2 _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION def get_characteristic_types(self) -> list[str]: @@ -293,7 +293,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): @property def default_name(self) -> str: """Return the default name of the device.""" - return "CO2" + return "Carbon Dioxide" @property def native_value(self) -> int: @@ -301,7 +301,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL) -class HomeKitBatterySensor(HomeKitSensor, SensorEntity): +class HomeKitBatterySensor(HomeKitSensor): """Representation of a Homekit battery sensor.""" _attr_device_class = SensorDeviceClass.BATTERY diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py index 16bef749429..daa6d593988 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -7,6 +7,7 @@ service-label-index despite not being linked to a service-label. https://github.com/home-assistant/core/pull/39090 """ +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from tests.components.homekit_controller.common import ( @@ -41,6 +42,7 @@ async def test_aqara_switch_setup(hass): entity_id="sensor.programmable_switch_battery_sensor", friendly_name="Programmable Switch Battery Sensor", unique_id="homekit-111a1111a1a111-5", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="100", ), diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py index 2cb312fc7f5..fe3d1ea5efc 100644 --- a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -46,6 +46,7 @@ async def test_arlo_baby_setup(hass): entity_id="sensor.arlobabya0_battery", unique_id="homekit-00A0000000000-700", friendly_name="ArloBabyA0 Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="82", ), @@ -53,6 +54,7 @@ async def test_arlo_baby_setup(hass): entity_id="sensor.arlobabya0_humidity", unique_id="homekit-00A0000000000-900", friendly_name="ArloBabyA0 Humidity", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="60.099998", ), @@ -60,6 +62,7 @@ async def test_arlo_baby_setup(hass): entity_id="sensor.arlobabya0_temperature", unique_id="homekit-00A0000000000-1000", friendly_name="ArloBabyA0 Temperature", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=TEMP_CELSIUS, state="24.0", ), diff --git a/tests/components/homekit_controller/specific_devices/test_eve_degree.py b/tests/components/homekit_controller/specific_devices/test_eve_degree.py index 51880bc076a..55377801529 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_degree.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_degree.py @@ -36,6 +36,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_temperature", unique_id="homekit-AA00A0A00000-22", friendly_name="Eve Degree AA11 Temperature", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=TEMP_CELSIUS, state="22.7719116210938", ), @@ -43,6 +44,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_humidity", unique_id="homekit-AA00A0A00000-27", friendly_name="Eve Degree AA11 Humidity", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="59.4818115234375", ), @@ -58,6 +60,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_battery", unique_id="homekit-AA00A0A00000-17", friendly_name="Eve Degree AA11 Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="65", ), diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 52a5fb83972..e64dc8378c5 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -1,5 +1,6 @@ """Tests for handling accessories on a Hue bridge via HomeKit.""" +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from tests.components.homekit_controller.common import ( @@ -41,6 +42,7 @@ async def test_hue_bridge_setup(hass): entities=[ EntityTestInfo( entity_id="sensor.hue_dimmer_switch_battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, friendly_name="Hue dimmer switch battery", unique_id="homekit-6623462389072572-644245094400", unit_of_measurement=PERCENTAGE, diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index eb9c36d0b79..155eb1c5c27 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -5,6 +5,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, ) +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from tests.components.homekit_controller.common import ( @@ -54,6 +55,7 @@ async def test_ryse_smart_bridge_setup(hass): EntityTestInfo( entity_id="sensor.master_bath_south_ryse_shade_battery", friendly_name="Master Bath South RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="100", @@ -80,6 +82,7 @@ async def test_ryse_smart_bridge_setup(hass): EntityTestInfo( entity_id="sensor.ryse_smartshade_ryse_shade_battery", friendly_name="RYSE SmartShade RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -129,6 +132,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="sensor.lr_left_ryse_shade_battery", friendly_name="LR Left RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="89", @@ -155,6 +159,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="sensor.lr_right_ryse_shade_battery", friendly_name="LR Right RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -181,6 +186,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="sensor.br_left_ryse_shade_battery", friendly_name="BR Left RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-4-64", unit_of_measurement=PERCENTAGE, state="100", @@ -206,6 +212,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): ), EntityTestInfo( entity_id="sensor.rzss_ryse_shade_battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, friendly_name="RZSS RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-5-64", unit_of_measurement=PERCENTAGE, diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py index 21aa91fc933..07c35fb867d 100644 --- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, ) +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, @@ -75,6 +76,7 @@ async def test_velux_cover_setup(hass): EntityTestInfo( entity_id="sensor.velux_sensor_temperature_sensor", friendly_name="VELUX Sensor Temperature sensor", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-a11b111-8", unit_of_measurement=TEMP_CELSIUS, state="18.9", @@ -82,6 +84,7 @@ async def test_velux_cover_setup(hass): EntityTestInfo( entity_id="sensor.velux_sensor_humidity_sensor", friendly_name="VELUX Sensor Humidity sensor", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-a11b111-11", unit_of_measurement=PERCENTAGE, state="58", @@ -89,6 +92,7 @@ async def test_velux_cover_setup(hass): EntityTestInfo( entity_id="sensor.velux_sensor_carbon_dioxide_sensor", friendly_name="VELUX Sensor Carbon Dioxide sensor", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-a11b111-14", unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state="400", diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 145d85eeed7..836da1e466f 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -3,7 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode -from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from tests.components.homekit_controller.common import Helper, setup_test_component @@ -79,6 +79,7 @@ async def test_temperature_sensor_read_state(hass, utcnow): assert state.state == "20" assert state.attributes["device_class"] == SensorDeviceClass.TEMPERATURE + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT async def test_temperature_sensor_not_added_twice(hass, utcnow): @@ -146,7 +147,7 @@ async def test_light_level_sensor_read_state(hass, utcnow): async def test_carbon_dioxide_level_sensor_read_state(hass, utcnow): """Test reading the state of a HomeKit carbon dioxide sensor accessory.""" helper = await setup_test_component( - hass, create_carbon_dioxide_level_sensor_service, suffix="co2" + hass, create_carbon_dioxide_level_sensor_service, suffix="carbon_dioxide" ) state = await helper.async_update( From 2951a941b615cd4ab6eb6c06e399cc9f4135fa09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 23 Jul 2022 18:44:58 +0200 Subject: [PATCH 650/820] Import correct scan interval in traccar (#75660) --- homeassistant/components/traccar/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 0ed13bceefa..50b0e7827aa 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -10,6 +10,7 @@ from stringcase import camelcase import voluptuous as vol from homeassistant.components.device_tracker import ( + CONF_SCAN_INTERVAL, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SOURCE_TYPE_GPS, ) @@ -21,7 +22,6 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, - CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, From 240bbfa0805ef28bf448abe7687904be3776e9f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 12:00:34 -0500 Subject: [PATCH 651/820] Retry later if bluetooth fails to start (#75647) --- .../components/bluetooth/__init__.py | 6 ++- tests/components/bluetooth/test_init.py | 42 +++++++++++++++++-- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index a50be7f4ace..901b8ee0644 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -23,6 +23,7 @@ from homeassistant.core import ( HomeAssistant, callback as hass_callback, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo @@ -338,7 +339,6 @@ class BluetoothManager: except (FileNotFoundError, BleakError) as ex: raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex install_multiple_bleak_catcher() - self.async_setup_unavailable_tracking() # We have to start it right away as some integrations might # need it straight away. _LOGGER.debug("Starting bluetooth scanner") @@ -349,7 +349,9 @@ class BluetoothManager: try: await self.scanner.start() except (FileNotFoundError, BleakError) as ex: - raise RuntimeError(f"Failed to start Bluetooth: {ex}") from ex + self._cancel_device_detected() + raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex + self.async_setup_unavailable_tracking() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) @hass_callback diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 5d932c56349..6dbec251026 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -15,6 +15,7 @@ from homeassistant.components.bluetooth import ( async_track_unavailable, models, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.setup import async_setup_component @@ -70,10 +71,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): async def test_setup_and_stop_broken_bluetooth(hass, caplog): """Test we fail gracefully when bluetooth/dbus is broken.""" - mock_bt = [ - {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} - ] - + mock_bt = [] with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( "homeassistant.components.bluetooth.HaBleakScanner.start", side_effect=BleakError, @@ -93,6 +91,42 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): assert len(bluetooth.async_discovered_service_info(hass)) == 0 +async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): + """Test we retry if the adapter is not yet available.""" + mock_bt = [] + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BleakError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + + assert "Failed to start Bluetooth" in caplog.text + assert len(bluetooth.async_discovered_service_info(hass)) == 0 + assert entry.state == ConfigEntryState.SETUP_RETRY + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop", + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] From 8300d5b89e621b0905c4db3a7949e89f45b7c872 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 12:59:35 -0500 Subject: [PATCH 652/820] Add bluetooth connection constant to the device registry (#75666) --- homeassistant/helpers/device_registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a19f476495a..f133eba8f92 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -38,6 +38,7 @@ STORAGE_VERSION_MINOR = 3 SAVE_DELAY = 10 CLEANUP_DELAY = 10 +CONNECTION_BLUETOOTH = "bluetooth" CONNECTION_NETWORK_MAC = "mac" CONNECTION_UPNP = "upnp" CONNECTION_ZIGBEE = "zigbee" From c5afaa2e6a1af77a69a0151dc1b8d7f5e3dcf2da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 13:03:01 -0500 Subject: [PATCH 653/820] Refactor PassiveBluetoothDataUpdateCoordinator to support multiple platforms (#75642) --- .../bluetooth/passive_update_coordinator.py | 235 ++++++++------ homeassistant/components/inkbird/__init__.py | 18 +- homeassistant/components/inkbird/sensor.py | 27 +- .../components/sensorpush/__init__.py | 18 +- homeassistant/components/sensorpush/sensor.py | 27 +- .../components/xiaomi_ble/__init__.py | 18 +- homeassistant/components/xiaomi_ble/sensor.py | 27 +- .../test_passive_update_coordinator.py | 307 ++++++++++++++---- 8 files changed, 443 insertions(+), 234 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index b2fdd5d7d58..507350f2162 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -55,32 +55,11 @@ class PassiveBluetoothDataUpdate(Generic[_T]): ) -_PassiveBluetoothDataUpdateCoordinatorT = TypeVar( - "_PassiveBluetoothDataUpdateCoordinatorT", - bound="PassiveBluetoothDataUpdateCoordinator[Any]", -) - - -class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): +class PassiveBluetoothDataUpdateCoordinator: """Passive bluetooth data update coordinator for bluetooth advertisements. - The coordinator is responsible for keeping track of the bluetooth data, - updating subscribers, and device availability. - - The update_method must return a PassiveBluetoothDataUpdate object. Callers - are responsible for formatting the data returned from their parser into - the appropriate format. - - The coordinator will call the update_method every time the bluetooth device - receives a new advertisement with the following signature: - - update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate - - As the size of each advertisement is limited, the update_method should - return a PassiveBluetoothDataUpdate object that contains only data that - should be updated. The coordinator will then dispatch subscribers based - on the data in the PassiveBluetoothDataUpdate object. The accumulated data - is available in the devices, entity_data, and entity_descriptions attributes. + The coordinator is responsible for dispatching the bluetooth data, + to each processor, and tracking devices. """ def __init__( @@ -88,45 +67,22 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): hass: HomeAssistant, logger: logging.Logger, address: str, - update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], ) -> None: """Initialize the coordinator.""" self.hass = hass self.logger = logger self.name: str | None = None self.address = address - self._listeners: list[ - Callable[[PassiveBluetoothDataUpdate[_T] | None], None] - ] = [] - self._entity_key_listeners: dict[ - PassiveBluetoothEntityKey, - list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], - ] = {} - self.update_method = update_method - - self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} - self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} - self.entity_descriptions: dict[ - PassiveBluetoothEntityKey, EntityDescription - ] = {} - self.devices: dict[str | None, DeviceInfo] = {} - - self.last_update_success = True + self._processors: list[PassiveBluetoothDataProcessor] = [] self._cancel_track_unavailable: CALLBACK_TYPE | None = None self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None - self.present = False + self._present = False self.last_seen = 0.0 @property def available(self) -> bool: """Return if the device is available.""" - return self.present and self.last_update_success - - @callback - def _async_handle_unavailable(self, _address: str) -> None: - """Handle the device going unavailable.""" - self.present = False - self.async_update_listeners(None) + return self._present @callback def _async_start(self) -> None: @@ -152,10 +108,121 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self._cancel_track_unavailable() self._cancel_track_unavailable = None + @callback + def async_register_processor( + self, processor: PassiveBluetoothDataProcessor + ) -> Callable[[], None]: + """Register a processor that subscribes to updates.""" + processor.coordinator = self + + @callback + def remove_processor() -> None: + """Remove a processor.""" + self._processors.remove(processor) + self._async_handle_processors_changed() + + self._processors.append(processor) + self._async_handle_processors_changed() + return remove_processor + + @callback + def _async_handle_processors_changed(self) -> None: + """Handle processors changed.""" + running = bool(self._cancel_bluetooth_advertisements) + if running and not self._processors: + self._async_stop() + elif not running and self._processors: + self._async_start() + + @callback + def _async_handle_unavailable(self, _address: str) -> None: + """Handle the device going unavailable.""" + self._present = False + for processor in self._processors: + processor.async_handle_unavailable() + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.last_seen = time.monotonic() + self.name = service_info.name + self._present = True + if self.hass.is_stopping: + return + for processor in self._processors: + processor.async_handle_bluetooth_event(service_info, change) + + +_PassiveBluetoothDataProcessorT = TypeVar( + "_PassiveBluetoothDataProcessorT", + bound="PassiveBluetoothDataProcessor[Any]", +) + + +class PassiveBluetoothDataProcessor(Generic[_T]): + """Passive bluetooth data processor for bluetooth advertisements. + + The processor is responsible for keeping track of the bluetooth data + and updating subscribers. + + The update_method must return a PassiveBluetoothDataUpdate object. Callers + are responsible for formatting the data returned from their parser into + the appropriate format. + + The processor will call the update_method every time the bluetooth device + receives a new advertisement data from the coordinator with the following signature: + + update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + + As the size of each advertisement is limited, the update_method should + return a PassiveBluetoothDataUpdate object that contains only data that + should be updated. The coordinator will then dispatch subscribers based + on the data in the PassiveBluetoothDataUpdate object. The accumulated data + is available in the devices, entity_data, and entity_descriptions attributes. + """ + + coordinator: PassiveBluetoothDataUpdateCoordinator + + def __init__( + self, + update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + ) -> None: + """Initialize the coordinator.""" + self.coordinator: PassiveBluetoothDataUpdateCoordinator + self._listeners: list[ + Callable[[PassiveBluetoothDataUpdate[_T] | None], None] + ] = [] + self._entity_key_listeners: dict[ + PassiveBluetoothEntityKey, + list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], + ] = {} + self.update_method = update_method + self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} + self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} + self.entity_descriptions: dict[ + PassiveBluetoothEntityKey, EntityDescription + ] = {} + self.devices: dict[str | None, DeviceInfo] = {} + self.last_update_success = True + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self.coordinator.available and self.last_update_success + + @callback + def async_handle_unavailable(self) -> None: + """Handle the device going unavailable.""" + self.async_update_listeners(None) + @callback def async_add_entities_listener( self, - entity_class: type[PassiveBluetoothCoordinatorEntity], + entity_class: type[PassiveBluetoothProcessorEntity], async_add_entites: AddEntitiesCallback, ) -> Callable[[], None]: """Add a listener for new entities.""" @@ -168,7 +235,7 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): """Listen for new entities.""" if data is None: return - entities: list[PassiveBluetoothCoordinatorEntity] = [] + entities: list[PassiveBluetoothProcessorEntity] = [] for entity_key, description in data.entity_descriptions.items(): if entity_key not in created: entities.append(entity_class(self, entity_key, description)) @@ -189,22 +256,10 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): def remove_listener() -> None: """Remove update listener.""" self._listeners.remove(update_callback) - self._async_handle_listeners_changed() self._listeners.append(update_callback) - self._async_handle_listeners_changed() return remove_listener - @callback - def _async_handle_listeners_changed(self) -> None: - """Handle listeners changed.""" - has_listeners = self._listeners or self._entity_key_listeners - running = bool(self._cancel_bluetooth_advertisements) - if running and not has_listeners: - self._async_stop() - elif not running and has_listeners: - self._async_start() - @callback def async_add_entity_key_listener( self, @@ -219,10 +274,8 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self._entity_key_listeners[entity_key].remove(update_callback) if not self._entity_key_listeners[entity_key]: del self._entity_key_listeners[entity_key] - self._async_handle_listeners_changed() self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) - self._async_handle_listeners_changed() return remove_listener @callback @@ -240,36 +293,32 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): update_callback(data) @callback - def _async_handle_bluetooth_event( + def async_handle_bluetooth_event( self, service_info: BluetoothServiceInfo, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" - self.last_seen = time.monotonic() - self.name = service_info.name - self.present = True - if self.hass.is_stopping: - return - try: new_data = self.update_method(service_info) except Exception as err: # pylint: disable=broad-except self.last_update_success = False - self.logger.exception( - "Unexpected error updating %s data: %s", self.name, err + self.coordinator.logger.exception( + "Unexpected error updating %s data: %s", self.coordinator.name, err ) return if not isinstance(new_data, PassiveBluetoothDataUpdate): self.last_update_success = False # type: ignore[unreachable] raise ValueError( - f"The update_method for {self.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" + f"The update_method for {self.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" ) if not self.last_update_success: self.last_update_success = True - self.logger.info("Processing %s data recovered", self.name) + self.coordinator.logger.info( + "Processing %s data recovered", self.coordinator.name + ) self.devices.update(new_data.devices) self.entity_descriptions.update(new_data.entity_descriptions) @@ -278,29 +327,27 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self.async_update_listeners(new_data) -class PassiveBluetoothCoordinatorEntity( - Entity, Generic[_PassiveBluetoothDataUpdateCoordinatorT] -): - """A class for entities using PassiveBluetoothDataUpdateCoordinator.""" +class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]): + """A class for entities using PassiveBluetoothDataProcessor.""" _attr_has_entity_name = True _attr_should_poll = False def __init__( self, - coordinator: _PassiveBluetoothDataUpdateCoordinatorT, + processor: _PassiveBluetoothDataProcessorT, entity_key: PassiveBluetoothEntityKey, description: EntityDescription, context: Any = None, ) -> None: - """Create the entity with a PassiveBluetoothDataUpdateCoordinator.""" + """Create the entity with a PassiveBluetoothDataProcessor.""" self.entity_description = description self.entity_key = entity_key - self.coordinator = coordinator - self.coordinator_context = context - address = coordinator.address + self.processor = processor + self.processor_context = context + address = processor.coordinator.address device_id = entity_key.device_id - devices = coordinator.devices + devices = processor.devices key = entity_key.key if device_id in devices: base_device_info = devices[device_id] @@ -317,26 +364,26 @@ class PassiveBluetoothCoordinatorEntity( ) self._attr_unique_id = f"{address}-{key}" if ATTR_NAME not in self._attr_device_info: - self._attr_device_info[ATTR_NAME] = self.coordinator.name - self._attr_name = coordinator.entity_names.get(entity_key) + self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name + self._attr_name = processor.entity_names.get(entity_key) @property def available(self) -> bool: """Return if entity is available.""" - return self.coordinator.available + return self.processor.available async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() self.async_on_remove( - self.coordinator.async_add_entity_key_listener( - self._handle_coordinator_update, self.entity_key + self.processor.async_add_entity_key_listener( + self._handle_processor_update, self.entity_key ) ) @callback - def _handle_coordinator_update( + def _handle_processor_update( self, new_data: PassiveBluetoothDataUpdate | None ) -> None: - """Handle updated data from the coordinator.""" + """Handle updated data from the processor.""" self.async_write_ha_state() diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 29b63e34263..330d85b665c 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,19 +3,14 @@ from __future__ import annotations import logging -from inkbird_ble import INKBIRDBluetoothDeviceData - from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.core import HomeAssistant from .const import DOMAIN -from .sensor import sensor_update_to_bluetooth_data_update PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -26,22 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up INKBIRD BLE device from a config entry.""" address = entry.unique_id assert address is not None - - data = INKBIRDBluetoothDeviceData() - - @callback - def _async_update_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Update data from INKBIRD Bluetooth.""" - return sensor_update_to_bluetooth_data_update(data.update(service_info)) - hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothDataUpdateCoordinator( hass, _LOGGER, - update_method=_async_update_data, address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 26311131181..600f28b6880 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -3,14 +3,22 @@ from __future__ import annotations from typing import Optional, Union -from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from inkbird_ble import ( + DeviceClass, + DeviceKey, + INKBIRDBluetoothDeviceData, + SensorDeviceInfo, + SensorUpdate, + Units, +) from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -121,16 +129,23 @@ async def async_setup_entry( coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ entry.entry_id ] + data = INKBIRDBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( - coordinator.async_add_entities_listener( + processor.async_add_entities_listener( INKBIRDBluetoothSensorEntity, async_add_entities ) ) class INKBIRDBluetoothSensorEntity( - PassiveBluetoothCoordinatorEntity[ - PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] ], SensorEntity, ): @@ -139,4 +154,4 @@ class INKBIRDBluetoothSensorEntity( @property def native_value(self) -> int | float | None: """Return the native value.""" - return self.coordinator.entity_data.get(self.entity_key) + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 89e2287be70..94e98e30f82 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,19 +3,14 @@ from __future__ import annotations import logging -from sensorpush_ble import SensorPushBluetoothDeviceData - from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.core import HomeAssistant from .const import DOMAIN -from .sensor import sensor_update_to_bluetooth_data_update PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -26,22 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SensorPush BLE device from a config entry.""" address = entry.unique_id assert address is not None - - data = SensorPushBluetoothDeviceData() - - @callback - def _async_update_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Update data from SensorPush Bluetooth.""" - return sensor_update_to_bluetooth_data_update(data.update(service_info)) - hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothDataUpdateCoordinator( hass, _LOGGER, - update_method=_async_update_data, address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 147555990e4..f592b594289 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -3,14 +3,22 @@ from __future__ import annotations from typing import Optional, Union -from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from sensorpush_ble import ( + DeviceClass, + DeviceKey, + SensorDeviceInfo, + SensorPushBluetoothDeviceData, + SensorUpdate, + Units, +) from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -122,16 +130,23 @@ async def async_setup_entry( coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ entry.entry_id ] + data = SensorPushBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( - coordinator.async_add_entities_listener( + processor.async_add_entities_listener( SensorPushBluetoothSensorEntity, async_add_entities ) ) class SensorPushBluetoothSensorEntity( - PassiveBluetoothCoordinatorEntity[ - PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] ], SensorEntity, ): @@ -140,4 +155,4 @@ class SensorPushBluetoothSensorEntity( @property def native_value(self) -> int | float | None: """Return the native value.""" - return self.coordinator.entity_data.get(self.entity_key) + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 036ff37a306..d7f3e4071aa 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,19 +3,14 @@ from __future__ import annotations import logging -from xiaomi_ble import XiaomiBluetoothDeviceData - from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.core import HomeAssistant from .const import DOMAIN -from .sensor import sensor_update_to_bluetooth_data_update PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -26,22 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Xiaomi BLE device from a config entry.""" address = entry.unique_id assert address is not None - - data = XiaomiBluetoothDeviceData() - - @callback - def _async_update_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Update data from Xiaomi Bluetooth.""" - return sensor_update_to_bluetooth_data_update(data.update(service_info)) - hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothDataUpdateCoordinator( hass, _LOGGER, - update_method=_async_update_data, address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index f485ccb8eb6..1452b5e9053 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -3,14 +3,22 @@ from __future__ import annotations from typing import Optional, Union -from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from xiaomi_ble import ( + DeviceClass, + DeviceKey, + SensorDeviceInfo, + SensorUpdate, + Units, + XiaomiBluetoothDeviceData, +) from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -150,16 +158,23 @@ async def async_setup_entry( coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ entry.entry_id ] + data = XiaomiBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( - coordinator.async_add_entities_listener( + processor.async_add_entities_listener( XiaomiBluetoothSensorEntity, async_add_entities ) ) class XiaomiBluetoothSensorEntity( - PassiveBluetoothCoordinatorEntity[ - PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] ], SensorEntity, ): @@ -168,4 +183,4 @@ class XiaomiBluetoothSensorEntity( @property def native_value(self) -> int | float | None: """Return the native value.""" - return self.coordinator.entity_data.get(self.entity_key) + return self.processor.entity_data.get(self.entity_key) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 48f2e8edf06..2b8c876460f 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -8,16 +8,21 @@ from unittest.mock import MagicMock, patch from home_assistant_bluetooth import BluetoothServiceInfo import pytest +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntityDescription, +) from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.const import TEMP_CELSIUS @@ -85,7 +90,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -95,40 +100,41 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + unregister_processor = coordinator.async_register_processor(processor) - entity_key = PassiveBluetoothEntityKey("temperature", None) - entity_key_events = [] - all_events = [] - mock_entity = MagicMock() - mock_add_entities = MagicMock() + entity_key = PassiveBluetoothEntityKey("temperature", None) + entity_key_events = [] + all_events = [] + mock_entity = MagicMock() + mock_add_entities = MagicMock() - def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock entity key listener.""" - entity_key_events.append(data) + def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock entity key listener.""" + entity_key_events.append(data) - cancel_async_add_entity_key_listener = ( - coordinator.async_add_entity_key_listener( - _async_entity_key_listener, - entity_key, - ) - ) + cancel_async_add_entity_key_listener = processor.async_add_entity_key_listener( + _async_entity_key_listener, + entity_key, + ) - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) - cancel_listener = coordinator.async_add_listener( - _all_listener, - ) + cancel_listener = processor.async_add_listener( + _all_listener, + ) - cancel_async_add_entities_listener = coordinator.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + cancel_async_add_entities_listener = processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) @@ -164,6 +170,8 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): assert len(mock_entity.mock_calls) == 2 assert coordinator.available is True + unregister_processor() + async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): """Test that the coordinator is unavailable after no data for a while.""" @@ -182,7 +190,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -192,23 +200,27 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + unregister_processor = coordinator.async_register_processor(processor) - mock_entity = MagicMock() - mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + mock_entity = MagicMock() + mock_add_entities = MagicMock() + processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) assert coordinator.available is False + assert processor.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True + assert processor.available is True scanner = _get_underlying_scanner() with patch( @@ -224,10 +236,12 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): ) await hass.async_block_till_done() assert coordinator.available is False + assert processor.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True + assert processor.available is True with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", @@ -242,6 +256,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): ) await hass.async_block_till_done() assert coordinator.available is False + assert processor.available is False + + unregister_processor() async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): @@ -256,7 +273,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -266,20 +283,23 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + unregister_processor = coordinator.async_register_processor(processor) - all_events = [] + all_events = [] - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) - coordinator.async_add_listener( - _all_listener, - ) + processor.async_add_listener( + _all_listener, + ) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 @@ -289,6 +309,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): # We should stop processing events once hass is stopping saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 + unregister_processor() async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): @@ -309,7 +330,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -319,23 +340,27 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_add_listener(MagicMock()) + unregister_processor = coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is True + assert processor.available is True # We should go unavailable once we get an exception saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert "Test exception" in caplog.text - assert coordinator.available is False + assert processor.available is False # We should go available again once we get data again saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is True + assert processor.available is True + unregister_processor() async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): @@ -356,7 +381,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -366,24 +391,28 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_add_listener(MagicMock()) + unregister_processor = coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is True + assert processor.available is True # We should go unavailable once we get bad data with pytest.raises(ValueError): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is False + assert processor.available is False # We should go available again once we get good data again saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is True + assert processor.available is True + unregister_processor() GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( @@ -692,7 +721,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -702,16 +731,19 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_add_listener(MagicMock()) + coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, mock_add_entities, ) @@ -736,7 +768,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): *mock_add_entities.mock_calls[1][1][0], ] - entity_one: PassiveBluetoothCoordinatorEntity = entities[0] + entity_one: PassiveBluetoothProcessorEntity = entities[0] entity_one.hass = hass assert entity_one.available is True assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote" @@ -797,7 +829,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -807,17 +839,19 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + coordinator.async_register_processor(processor) - mock_add_entities = MagicMock() + mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, - mock_add_entities, - ) + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_entities, + ) saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) # First call with just the remote sensor entities results in them being added @@ -828,7 +862,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner assert len(mock_add_entities.mock_calls) == 1 entities = mock_add_entities.mock_calls[0][1][0] - entity_one: PassiveBluetoothCoordinatorEntity = entities[0] + entity_one: PassiveBluetoothProcessorEntity = entities[0] entity_one.hass = hass assert entity_one.available is True assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" @@ -857,7 +891,7 @@ async def test_passive_bluetooth_entity_with_entity_platform( return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -867,18 +901,19 @@ async def test_passive_bluetooth_entity_with_entity_platform( saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + coordinator.async_register_processor(processor) - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, - lambda entities: hass.async_create_task( - entity_platform.async_add_entities(entities) - ), - ) - + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + lambda entities: hass.async_create_task( + entity_platform.async_add_entities(entities) + ), + ) saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) await hass.async_block_till_done() saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) @@ -891,3 +926,133 @@ async def test_passive_bluetooth_entity_with_entity_platform( hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") is not None ) + + +SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_names={ + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("motion", None): True, + }, + entity_names={ + PassiveBluetoothEntityKey("motion", None): "Motion", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("motion", None): BinarySensorEntityDescription( + key="motion", + device_class=BinarySensorDeviceClass.MOTION, + ), + }, +) + + +async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start): + """Test integration of PassiveBluetoothDataUpdateCoordinator with multiple platforms.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + binary_sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + sesnor_processor = PassiveBluetoothDataProcessor( + lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(binary_sensor_processor) + coordinator.async_register_processor(sesnor_processor) + + binary_sensor_processor.async_add_listener(MagicMock()) + sesnor_processor.async_add_listener(MagicMock()) + + mock_add_sensor_entities = MagicMock() + mock_add_binary_sensor_entities = MagicMock() + + sesnor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_sensor_entities, + ) + binary_sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_binary_sensor_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_binary_sensor_entities.mock_calls) == 1 + assert len(mock_add_sensor_entities.mock_calls) == 1 + + binary_sesnor_entities = [ + *mock_add_binary_sensor_entities.mock_calls[0][1][0], + ] + sesnor_entities = [ + *mock_add_sensor_entities.mock_calls[0][1][0], + ] + + sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_entities[0] + sensor_entity_one.hass = hass + assert sensor_entity_one.available is True + assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" + assert sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="pressure", device_id=None + ) + + binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sesnor_entities[ + 0 + ] + binary_sensor_entity_one.hass = hass + assert binary_sensor_entity_one.available is True + assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" + assert binary_sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="motion", device_id=None + ) From 7cf2d1759dde088105f77ca61dba8e58e3474b83 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sun, 24 Jul 2022 00:44:48 +0300 Subject: [PATCH 654/820] Upgrade pyrisco to 0.5.0 (#75648) * Upgrade to pyrisco 0.4.0 * Parametrized error tests in config flow * Inline error parameters * Switch to RiscoCloud --- homeassistant/components/risco/__init__.py | 4 +- homeassistant/components/risco/config_flow.py | 4 +- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../risco/test_alarm_control_panel.py | 40 ++++++++--- tests/components/risco/test_binary_sensor.py | 8 +-- tests/components/risco/test_config_flow.py | 66 ++++++------------- tests/components/risco/test_sensor.py | 8 +-- tests/components/risco/util.py | 12 ++-- 10 files changed, 71 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 1932548a907..fea3ee63aac 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from pyrisco import CannotConnectError, OperationError, RiscoAPI, UnauthorizedError +from pyrisco import CannotConnectError, OperationError, RiscoCloud, UnauthorizedError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Risco from a config entry.""" data = entry.data - risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) + risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) try: await risco.login(async_get_clientsession(hass)) except CannotConnectError as error: diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index e2e139a19e0..5f8f40cb5f7 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from pyrisco import CannotConnectError, RiscoAPI, UnauthorizedError +from pyrisco import CannotConnectError, RiscoCloud, UnauthorizedError import voluptuous as vol from homeassistant import config_entries, core @@ -52,7 +52,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) + risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) try: await risco.login(async_get_clientsession(hass)) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 736adcf0c35..a7c07af3e18 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.3.1"], + "requirements": ["pyrisco==0.5.0"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 80ca6871b48..4309d2f34c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1785,7 +1785,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.3.1 +pyrisco==0.5.0 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d0e6f54715..4d8e790b50f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1222,7 +1222,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.3.1 +pyrisco==0.5.0 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 70ec7844624..4a82656147d 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -99,7 +99,7 @@ def two_part_alarm(): "partitions", new_callable=PropertyMock(return_value=partition_mocks), ), patch( - "homeassistant.components.risco.RiscoAPI.get_state", + "homeassistant.components.risco.RiscoCloud.get_state", return_value=alarm_mock, ): yield alarm_mock @@ -109,7 +109,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -125,7 +125,7 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -228,7 +228,7 @@ async def test_states(hass, two_part_alarm): async def _test_service_call( hass, service, method, entity_id, partition_id, *args, **kwargs ): - with patch(f"homeassistant.components.risco.RiscoAPI.{method}") as set_mock: + with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: await _call_alarm_service(hass, service, entity_id, **kwargs) set_mock.assert_awaited_once_with(partition_id, *args) @@ -236,7 +236,7 @@ async def _test_service_call( async def _test_no_service_call( hass, service, method, entity_id, partition_id, **kwargs ): - with patch(f"homeassistant.components.risco.RiscoAPI.{method}") as set_mock: + with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: await _call_alarm_service(hass, service, entity_id, **kwargs) set_mock.assert_not_awaited() @@ -302,10 +302,20 @@ async def test_sets_full_custom_mapping(hass, two_part_alarm): hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C" ) await _test_service_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", FIRST_ENTITY_ID, 0, "D" + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "group_arm", + FIRST_ENTITY_ID, + 0, + "D", ) await _test_service_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", SECOND_ENTITY_ID, 1, "D" + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "group_arm", + SECOND_ENTITY_ID, + 1, + "D", ) @@ -333,10 +343,22 @@ async def test_sets_with_correct_code(hass, two_part_alarm): hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1, **code ) await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, "C", **code + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + FIRST_ENTITY_ID, + 0, + "C", + **code, ) await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C", **code + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + SECOND_ENTITY_ID, + 1, + "C", + **code, ) with pytest.raises(HomeAssistantError): await _test_no_service_call( diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 7f68db7939d..a7c11c9cb00 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -20,7 +20,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -36,7 +36,7 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -106,7 +106,7 @@ async def test_states(hass, two_zone_alarm): # noqa: F811 async def test_bypass(hass, two_zone_alarm): # noqa: F811 """Test bypassing a zone.""" await setup_risco(hass) - with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} await hass.services.async_call( @@ -119,7 +119,7 @@ async def test_bypass(hass, two_zone_alarm): # noqa: F811 async def test_unbypass(hass, two_zone_alarm): # noqa: F811 """Test unbypassing a zone.""" await setup_risco(hass) - with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} await hass.services.async_call( diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 8dd7c4bf7f7..8d04f478e44 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -51,13 +51,13 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", + "homeassistant.components.risco.config_flow.RiscoCloud.login", return_value=True, ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.site_name", + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.close" + "homeassistant.components.risco.config_flow.RiscoCloud.close" ) as mock_close, patch( "homeassistant.components.risco.async_setup_entry", return_value=True, @@ -74,61 +74,33 @@ async def test_form(hass): mock_close.assert_awaited_once() -async def test_form_invalid_auth(hass): - """Test we handle invalid auth.""" +@pytest.mark.parametrize( + "exception, error", + [ + (UnauthorizedError, "invalid_auth"), + (CannotConnectError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_error(hass, exception, error): + """Test we handle config flow errors.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", - side_effect=UnauthorizedError, - ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: + "homeassistant.components.risco.config_flow.RiscoCloud.login", + side_effect=exception, + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ) as mock_close: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} mock_close.assert_awaited_once() - - -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", - side_effect=CannotConnectError, - ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} - mock_close.assert_awaited_once() - - -async def test_form_exception(hass): - """Test we handle unknown exception.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", - side_effect=Exception, - ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} - mock_close.assert_awaited_once() + assert result2["errors"] == {"base": error} async def test_form_already_exists(hass): diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 24efbabf087..8fb4daf8624 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -113,7 +113,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -130,7 +130,7 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -175,7 +175,7 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 assert not registry.async_is_registered(id) with patch( - "homeassistant.components.risco.RiscoAPI.site_uuid", + "homeassistant.components.risco.RiscoCloud.site_uuid", new_callable=PropertyMock(return_value=TEST_SITE_UUID), ), patch( "homeassistant.components.risco.Store.async_save", @@ -191,7 +191,7 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 _check_state(hass, category, entity_id) with patch( - "homeassistant.components.risco.RiscoAPI.get_events", return_value=[] + "homeassistant.components.risco.RiscoCloud.get_events", return_value=[] ) as events_mock, patch( "homeassistant.components.risco.Store.async_load", return_value={LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}, diff --git a/tests/components/risco/util.py b/tests/components/risco/util.py index 8b918f32c12..3fa81586d27 100644 --- a/tests/components/risco/util.py +++ b/tests/components/risco/util.py @@ -23,18 +23,18 @@ async def setup_risco(hass, events=[], options={}): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", return_value=True, ), patch( - "homeassistant.components.risco.RiscoAPI.site_uuid", + "homeassistant.components.risco.RiscoCloud.site_uuid", new_callable=PropertyMock(return_value=TEST_SITE_UUID), ), patch( - "homeassistant.components.risco.RiscoAPI.site_name", + "homeassistant.components.risco.RiscoCloud.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.RiscoAPI.close" + "homeassistant.components.risco.RiscoCloud.close" ), patch( - "homeassistant.components.risco.RiscoAPI.get_events", + "homeassistant.components.risco.RiscoCloud.get_events", return_value=events, ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -68,7 +68,7 @@ def two_zone_alarm(): "zones", new_callable=PropertyMock(return_value=zone_mocks), ), patch( - "homeassistant.components.risco.RiscoAPI.get_state", + "homeassistant.components.risco.RiscoCloud.get_state", return_value=alarm_mock, ): yield alarm_mock From 19f82e52016ebd7a35543d09a62b83d1c26197c8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 24 Jul 2022 00:28:19 +0000 Subject: [PATCH 655/820] [ci skip] Translation update --- .../components/bluetooth/translations/ca.json | 22 +++++++++++++++++++ .../components/bluetooth/translations/de.json | 6 +++++ .../components/bluetooth/translations/el.json | 6 +++++ .../components/bluetooth/translations/fr.json | 22 +++++++++++++++++++ .../components/bluetooth/translations/hu.json | 22 +++++++++++++++++++ .../components/bluetooth/translations/pl.json | 6 +++++ .../components/bluetooth/translations/ru.json | 22 +++++++++++++++++++ .../components/demo/translations/hu.json | 21 ++++++++++++++++++ .../components/demo/translations/ru.json | 15 +++++++++++++ .../components/inkbird/translations/ca.json | 21 ++++++++++++++++++ .../components/inkbird/translations/fr.json | 21 ++++++++++++++++++ .../components/inkbird/translations/hu.json | 21 ++++++++++++++++++ .../components/inkbird/translations/ru.json | 21 ++++++++++++++++++ .../components/plugwise/translations/hu.json | 3 ++- .../components/plugwise/translations/ru.json | 3 ++- .../sensorpush/translations/ca.json | 21 ++++++++++++++++++ .../sensorpush/translations/fr.json | 21 ++++++++++++++++++ .../sensorpush/translations/hu.json | 21 ++++++++++++++++++ .../sensorpush/translations/ru.json | 21 ++++++++++++++++++ .../components/uscis/translations/ca.json | 1 + .../components/uscis/translations/hu.json | 8 +++++++ .../components/uscis/translations/ru.json | 8 +++++++ .../xiaomi_ble/translations/ca.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/de.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/el.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/fr.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/hu.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/id.json | 7 ++++++ .../xiaomi_ble/translations/pl.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/ru.json | 21 ++++++++++++++++++ .../components/zha/translations/ca.json | 1 + .../components/zha/translations/hu.json | 1 + .../components/zha/translations/ru.json | 1 + 33 files changed, 488 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bluetooth/translations/ca.json create mode 100644 homeassistant/components/bluetooth/translations/fr.json create mode 100644 homeassistant/components/bluetooth/translations/hu.json create mode 100644 homeassistant/components/bluetooth/translations/ru.json create mode 100644 homeassistant/components/inkbird/translations/ca.json create mode 100644 homeassistant/components/inkbird/translations/fr.json create mode 100644 homeassistant/components/inkbird/translations/hu.json create mode 100644 homeassistant/components/inkbird/translations/ru.json create mode 100644 homeassistant/components/sensorpush/translations/ca.json create mode 100644 homeassistant/components/sensorpush/translations/fr.json create mode 100644 homeassistant/components/sensorpush/translations/hu.json create mode 100644 homeassistant/components/sensorpush/translations/ru.json create mode 100644 homeassistant/components/uscis/translations/hu.json create mode 100644 homeassistant/components/uscis/translations/ru.json create mode 100644 homeassistant/components/xiaomi_ble/translations/ca.json create mode 100644 homeassistant/components/xiaomi_ble/translations/de.json create mode 100644 homeassistant/components/xiaomi_ble/translations/el.json create mode 100644 homeassistant/components/xiaomi_ble/translations/fr.json create mode 100644 homeassistant/components/xiaomi_ble/translations/hu.json create mode 100644 homeassistant/components/xiaomi_ble/translations/id.json create mode 100644 homeassistant/components/xiaomi_ble/translations/pl.json create mode 100644 homeassistant/components/xiaomi_ble/translations/ru.json diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json new file mode 100644 index 00000000000..8fb4c022e0a --- /dev/null +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "enable_bluetooth": { + "description": "Vols configurar Bluetooth?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index 1c677be6828..ef305862210 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "enable_bluetooth": { + "description": "M\u00f6chtest du Bluetooth einrichten?" + }, "user": { "data": { "address": "Ger\u00e4t" diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 05198f0929a..03b3be612a3 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "enable_bluetooth": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Bluetooth;" + }, "user": { "data": { "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/bluetooth/translations/fr.json b/homeassistant/components/bluetooth/translations/fr.json new file mode 100644 index 00000000000..80a0ac6ea3f --- /dev/null +++ b/homeassistant/components/bluetooth/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "enable_bluetooth": { + "description": "Voulez-vous configurer le Bluetooth\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json new file mode 100644 index 00000000000..5cdc199476c --- /dev/null +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "enable_bluetooth": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a Bluetooth-ot?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index 4b93cce9f7e..99ad564ebd9 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "enable_bluetooth": { + "description": "Czy chcesz skonfigurowa\u0107 Bluetooth?" + }, "user": { "data": { "address": "Urz\u0105dzenie" diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json new file mode 100644 index 00000000000..972b858718c --- /dev/null +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "enable_bluetooth": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Bluetooth?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index 87810814aac..1ff83e4ed06 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Nyomja meg az OK gombot, ha a villog\u00f3 folyad\u00e9kot felt\u00f6lt\u00f6tt\u00e9k.", + "title": "A villog\u00f3 folyad\u00e9kot fel kell t\u00f6lteni" + } + } + }, + "title": "A villog\u00f3 folyad\u00e9k ki\u00fcr\u00fclt, \u00e9s \u00fajra kell t\u00f6lteni" + }, + "transmogrifier_deprecated": { + "description": "A transzmogrifier komponens az \u00faj API-ban el\u00e9rhet\u0151 helyi vez\u00e9rl\u00e9s hi\u00e1nya miatt elavult", + "title": "A transzmogrifier komponens elavult" + }, + "unfixable_problem": { + "description": "Ez az eset soha nem fog le\u00e1llni.", + "title": "Ez a hiba nem jav\u00edthat\u00f3" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index e3bd96b880a..c08143d6b41 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -1,4 +1,19 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 OK, \u043a\u043e\u0433\u0434\u0430 \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430.", + "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u043b\u0438\u0442\u044c \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432" + } + } + } + }, + "unfixable_problem": { + "title": "\u042d\u0442\u043e \u043d\u0435 \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/inkbird/translations/ca.json b/homeassistant/components/inkbird/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/inkbird/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/fr.json b/homeassistant/components/inkbird/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/inkbird/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/hu.json b/homeassistant/components/inkbird/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/inkbird/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/ru.json b/homeassistant/components/inkbird/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/inkbird/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index b11137a2658..c97911bad09 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "anna_with_adam": "Anna \u00e9s \u00c1d\u00e1m is \u00e9szlelve lett. Adja hozz\u00e1 az \u00c1d\u00e1mot az Anna helyett" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index c7cfa232fd0..92d78e7f4ad 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "anna_with_adam": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0410\u043d\u043d\u0430 \u0438 \u0410\u0434\u0430\u043c. \u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0410\u0434\u0430\u043c\u0430 \u0432\u043c\u0435\u0441\u0442\u043e \u0410\u043d\u043d\u044b." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/sensorpush/translations/ca.json b/homeassistant/components/sensorpush/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/sensorpush/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/fr.json b/homeassistant/components/sensorpush/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/sensorpush/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/hu.json b/homeassistant/components/sensorpush/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/ru.json b/homeassistant/components/sensorpush/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ca.json b/homeassistant/components/uscis/translations/ca.json index 7c836867d89..2072cfb0d92 100644 --- a/homeassistant/components/uscis/translations/ca.json +++ b/homeassistant/components/uscis/translations/ca.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "La integraci\u00f3 de Serveis de Ciutadania i Immigraci\u00f3 dels Estats Units (USCIS) est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 es basa en el 'webscraping', que no est\u00e0 adm\u00e8s. \n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", "title": "S'est\u00e0 eliminant la integraci\u00f3 USCIS" } } diff --git a/homeassistant/components/uscis/translations/hu.json b/homeassistant/components/uscis/translations/hu.json new file mode 100644 index 00000000000..181cbaf076f --- /dev/null +++ b/homeassistant/components/uscis/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A U.S. Citizenship and Immigration Services (USCIS) integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a Home Assistant 2022.10-t\u0151l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sa az\u00e9rt t\u00f6rt\u00e9nik, mert webscrapingre t\u00e1maszkodik, ami nem megengedett.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az USCIS integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ru.json b/homeassistant/components/uscis/translations/ru.json new file mode 100644 index 00000000000..f3b70020245 --- /dev/null +++ b/homeassistant/components/uscis/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0421\u043b\u0443\u0436\u0431\u044b \u0433\u0440\u0430\u0436\u0434\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u0438\u043c\u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0421\u0428\u0410 (USCIS) \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f USCIS \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/el.json b/homeassistant/components/xiaomi_ble/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/fr.json b/homeassistant/components/xiaomi_ble/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json new file mode 100644 index 00000000000..8b24900933b --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Alur konfigurasi sedang berlangsung" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pl.json b/homeassistant/components/xiaomi_ble/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ru.json b/homeassistant/components/xiaomi_ble/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 2fe8d1a151c..4167704e4a0 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Considera els dispositius connectats a la xarxa el\u00e8ctrica com a no disponibles al cap de (segons)", "default_light_transition": "Temps de transici\u00f3 predeterminat (segons)", "enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa", + "enhanced_light_transition": "Activa la transici\u00f3 millorada de color/temperatura de llum des de l'estat apagat", "title": "Opcions globals" } }, diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index d446de8e17a..61b0f0c0c9d 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "H\u00e1l\u00f3zati t\u00e1pell\u00e1t\u00e1s\u00fa eszk\u00f6z\u00f6k nem el\u00e9rhet\u0151 \u00e1llapot\u00faak ennyi id\u0151 ut\u00e1n (mp.)", "default_light_transition": "Alap\u00e9rtelmezett f\u00e9ny-\u00e1tmeneti id\u0151 (m\u00e1sodpercben)", "enable_identify_on_join": "Azonos\u00edt\u00f3 hat\u00e1s, amikor az eszk\u00f6z\u00f6k csatlakoznak a h\u00e1l\u00f3zathoz", + "enhanced_light_transition": "F\u00e9ny sz\u00edn/sz\u00ednh\u0151m\u00e9rs\u00e9klet \u00e1tmenete kikapcsolt \u00e1llapotb\u00f3l", "title": "Glob\u00e1lis be\u00e1ll\u00edt\u00e1sok" } }, diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index be7fb30dbec..83ba818087a 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", + "enhanced_light_transition": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u0446\u0432\u0435\u0442\u0430/\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0441\u0432\u0435\u0442\u0430 \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" } }, From da131beced42203630778aa1460c1767200f00ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 19:33:47 -0500 Subject: [PATCH 656/820] Split bluetooth coordinator into two classes (#75675) --- .../bluetooth/passive_update_coordinator.py | 397 +------ .../bluetooth/passive_update_processor.py | 346 ++++++ .../bluetooth/update_coordinator.py | 84 ++ homeassistant/components/inkbird/__init__.py | 6 +- homeassistant/components/inkbird/sensor.py | 6 +- .../components/sensorpush/__init__.py | 6 +- homeassistant/components/sensorpush/sensor.py | 6 +- .../components/xiaomi_ble/__init__.py | 6 +- homeassistant/components/xiaomi_ble/sensor.py | 6 +- .../test_passive_update_coordinator.py | 1033 ++-------------- .../test_passive_update_processor.py | 1058 +++++++++++++++++ tests/components/inkbird/test_sensor.py | 2 +- tests/components/sensorpush/test_sensor.py | 2 +- tests/components/xiaomi_ble/test_sensor.py | 4 +- 14 files changed, 1667 insertions(+), 1295 deletions(-) create mode 100644 homeassistant/components/bluetooth/passive_update_processor.py create mode 100644 homeassistant/components/bluetooth/update_coordinator.py create mode 100644 tests/components/bluetooth/test_passive_update_processor.py diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 507350f2162..4a22f03449e 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,65 +1,23 @@ -"""The Bluetooth integration.""" +"""Passive update coordinator for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Mapping -import dataclasses +from collections.abc import Callable, Generator import logging -import time -from typing import Any, Generic, TypeVar +from typing import Any -from home_assistant_bluetooth import BluetoothServiceInfo - -from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ( - BluetoothCallbackMatcher, - BluetoothChange, - async_register_callback, - async_track_unavailable, -) -from .const import DOMAIN +from . import BluetoothChange +from .update_coordinator import BasePassiveBluetoothCoordinator -@dataclasses.dataclass(frozen=True) -class PassiveBluetoothEntityKey: - """Key for a passive bluetooth entity. +class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): + """Class to manage passive bluetooth advertisements. - Example: - key: temperature - device_id: outdoor_sensor_1 - """ - - key: str - device_id: str | None - - -_T = TypeVar("_T") - - -@dataclasses.dataclass(frozen=True) -class PassiveBluetoothDataUpdate(Generic[_T]): - """Generic bluetooth data.""" - - devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) - entity_descriptions: Mapping[ - PassiveBluetoothEntityKey, EntityDescription - ] = dataclasses.field(default_factory=dict) - entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field( - default_factory=dict - ) - entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field( - default_factory=dict - ) - - -class PassiveBluetoothDataUpdateCoordinator: - """Passive bluetooth data update coordinator for bluetooth advertisements. - - The coordinator is responsible for dispatching the bluetooth data, - to each processor, and tracking devices. + This coordinator is responsible for dispatching the bluetooth data + and tracking devices. """ def __init__( @@ -68,78 +26,52 @@ class PassiveBluetoothDataUpdateCoordinator: logger: logging.Logger, address: str, ) -> None: - """Initialize the coordinator.""" - self.hass = hass - self.logger = logger - self.name: str | None = None - self.address = address - self._processors: list[PassiveBluetoothDataProcessor] = [] - self._cancel_track_unavailable: CALLBACK_TYPE | None = None - self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None - self._present = False - self.last_seen = 0.0 - - @property - def available(self) -> bool: - """Return if the device is available.""" - return self._present + """Initialize PassiveBluetoothDataUpdateCoordinator.""" + super().__init__(hass, logger, address) + self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} @callback - def _async_start(self) -> None: - """Start the callbacks.""" - self._cancel_bluetooth_advertisements = async_register_callback( - self.hass, - self._async_handle_bluetooth_event, - BluetoothCallbackMatcher(address=self.address), - ) - self._cancel_track_unavailable = async_track_unavailable( - self.hass, - self._async_handle_unavailable, - self.address, - ) + def async_update_listeners(self) -> None: + """Update all registered listeners.""" + for update_callback, _ in list(self._listeners.values()): + update_callback() @callback - def _async_stop(self) -> None: - """Stop the callbacks.""" - if self._cancel_bluetooth_advertisements is not None: - self._cancel_bluetooth_advertisements() - self._cancel_bluetooth_advertisements = None - if self._cancel_track_unavailable is not None: - self._cancel_track_unavailable() - self._cancel_track_unavailable = None + def _async_handle_unavailable(self, address: str) -> None: + """Handle the device going unavailable.""" + super()._async_handle_unavailable(address) + self.async_update_listeners() @callback - def async_register_processor( - self, processor: PassiveBluetoothDataProcessor - ) -> Callable[[], None]: - """Register a processor that subscribes to updates.""" - processor.coordinator = self + def async_start(self) -> CALLBACK_TYPE: + """Start the data updater.""" + self._async_start() @callback - def remove_processor() -> None: - """Remove a processor.""" - self._processors.remove(processor) - self._async_handle_processors_changed() - - self._processors.append(processor) - self._async_handle_processors_changed() - return remove_processor - - @callback - def _async_handle_processors_changed(self) -> None: - """Handle processors changed.""" - running = bool(self._cancel_bluetooth_advertisements) - if running and not self._processors: + def _async_cancel() -> None: self._async_stop() - elif not running and self._processors: - self._async_start() + + return _async_cancel @callback - def _async_handle_unavailable(self, _address: str) -> None: - """Handle the device going unavailable.""" - self._present = False - for processor in self._processors: - processor.async_handle_unavailable() + def async_add_listener( + self, update_callback: CALLBACK_TYPE, context: Any = None + ) -> Callable[[], None]: + """Listen for data updates.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.pop(remove_listener) + + self._listeners[remove_listener] = (update_callback, context) + return remove_listener + + def async_contexts(self) -> Generator[Any, None, None]: + """Return all registered contexts.""" + yield from ( + context for _, context in self._listeners.values() if context is not None + ) @callback def _async_handle_bluetooth_event( @@ -148,242 +80,19 @@ class PassiveBluetoothDataUpdateCoordinator: change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" - self.last_seen = time.monotonic() - self.name = service_info.name - self._present = True - if self.hass.is_stopping: - return - for processor in self._processors: - processor.async_handle_bluetooth_event(service_info, change) + super()._async_handle_bluetooth_event(service_info, change) + self.async_update_listeners() -_PassiveBluetoothDataProcessorT = TypeVar( - "_PassiveBluetoothDataProcessorT", - bound="PassiveBluetoothDataProcessor[Any]", -) - - -class PassiveBluetoothDataProcessor(Generic[_T]): - """Passive bluetooth data processor for bluetooth advertisements. - - The processor is responsible for keeping track of the bluetooth data - and updating subscribers. - - The update_method must return a PassiveBluetoothDataUpdate object. Callers - are responsible for formatting the data returned from their parser into - the appropriate format. - - The processor will call the update_method every time the bluetooth device - receives a new advertisement data from the coordinator with the following signature: - - update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate - - As the size of each advertisement is limited, the update_method should - return a PassiveBluetoothDataUpdate object that contains only data that - should be updated. The coordinator will then dispatch subscribers based - on the data in the PassiveBluetoothDataUpdate object. The accumulated data - is available in the devices, entity_data, and entity_descriptions attributes. - """ +class PassiveBluetoothCoordinatorEntity(CoordinatorEntity): + """A class for entities using DataUpdateCoordinator.""" coordinator: PassiveBluetoothDataUpdateCoordinator - def __init__( - self, - update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], - ) -> None: - """Initialize the coordinator.""" - self.coordinator: PassiveBluetoothDataUpdateCoordinator - self._listeners: list[ - Callable[[PassiveBluetoothDataUpdate[_T] | None], None] - ] = [] - self._entity_key_listeners: dict[ - PassiveBluetoothEntityKey, - list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], - ] = {} - self.update_method = update_method - self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} - self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} - self.entity_descriptions: dict[ - PassiveBluetoothEntityKey, EntityDescription - ] = {} - self.devices: dict[str | None, DeviceInfo] = {} - self.last_update_success = True - - @property - def available(self) -> bool: - """Return if the device is available.""" - return self.coordinator.available and self.last_update_success - - @callback - def async_handle_unavailable(self) -> None: - """Handle the device going unavailable.""" - self.async_update_listeners(None) - - @callback - def async_add_entities_listener( - self, - entity_class: type[PassiveBluetoothProcessorEntity], - async_add_entites: AddEntitiesCallback, - ) -> Callable[[], None]: - """Add a listener for new entities.""" - created: set[PassiveBluetoothEntityKey] = set() - - @callback - def _async_add_or_update_entities( - data: PassiveBluetoothDataUpdate[_T] | None, - ) -> None: - """Listen for new entities.""" - if data is None: - return - entities: list[PassiveBluetoothProcessorEntity] = [] - for entity_key, description in data.entity_descriptions.items(): - if entity_key not in created: - entities.append(entity_class(self, entity_key, description)) - created.add(entity_key) - if entities: - async_add_entites(entities) - - return self.async_add_listener(_async_add_or_update_entities) - - @callback - def async_add_listener( - self, - update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], - ) -> Callable[[], None]: - """Listen for all updates.""" - - @callback - def remove_listener() -> None: - """Remove update listener.""" - self._listeners.remove(update_callback) - - self._listeners.append(update_callback) - return remove_listener - - @callback - def async_add_entity_key_listener( - self, - update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], - entity_key: PassiveBluetoothEntityKey, - ) -> Callable[[], None]: - """Listen for updates by device key.""" - - @callback - def remove_listener() -> None: - """Remove update listener.""" - self._entity_key_listeners[entity_key].remove(update_callback) - if not self._entity_key_listeners[entity_key]: - del self._entity_key_listeners[entity_key] - - self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) - return remove_listener - - @callback - def async_update_listeners( - self, data: PassiveBluetoothDataUpdate[_T] | None - ) -> None: - """Update all registered listeners.""" - # Dispatch to listeners without a filter key - for update_callback in self._listeners: - update_callback(data) - - # Dispatch to listeners with a filter key - for listeners in self._entity_key_listeners.values(): - for update_callback in listeners: - update_callback(data) - - @callback - def async_handle_bluetooth_event( - self, - service_info: BluetoothServiceInfo, - change: BluetoothChange, - ) -> None: - """Handle a Bluetooth event.""" - try: - new_data = self.update_method(service_info) - except Exception as err: # pylint: disable=broad-except - self.last_update_success = False - self.coordinator.logger.exception( - "Unexpected error updating %s data: %s", self.coordinator.name, err - ) - return - - if not isinstance(new_data, PassiveBluetoothDataUpdate): - self.last_update_success = False # type: ignore[unreachable] - raise ValueError( - f"The update_method for {self.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" - ) - - if not self.last_update_success: - self.last_update_success = True - self.coordinator.logger.info( - "Processing %s data recovered", self.coordinator.name - ) - - self.devices.update(new_data.devices) - self.entity_descriptions.update(new_data.entity_descriptions) - self.entity_data.update(new_data.entity_data) - self.entity_names.update(new_data.entity_names) - self.async_update_listeners(new_data) - - -class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]): - """A class for entities using PassiveBluetoothDataProcessor.""" - - _attr_has_entity_name = True - _attr_should_poll = False - - def __init__( - self, - processor: _PassiveBluetoothDataProcessorT, - entity_key: PassiveBluetoothEntityKey, - description: EntityDescription, - context: Any = None, - ) -> None: - """Create the entity with a PassiveBluetoothDataProcessor.""" - self.entity_description = description - self.entity_key = entity_key - self.processor = processor - self.processor_context = context - address = processor.coordinator.address - device_id = entity_key.device_id - devices = processor.devices - key = entity_key.key - if device_id in devices: - base_device_info = devices[device_id] - else: - base_device_info = DeviceInfo({}) - if device_id: - self._attr_device_info = base_device_info | DeviceInfo( - {ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}} - ) - self._attr_unique_id = f"{address}-{key}-{device_id}" - else: - self._attr_device_info = base_device_info | DeviceInfo( - {ATTR_IDENTIFIERS: {(DOMAIN, address)}} - ) - self._attr_unique_id = f"{address}-{key}" - if ATTR_NAME not in self._attr_device_info: - self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name - self._attr_name = processor.entity_names.get(entity_key) + async def async_update(self) -> None: + """All updates are passive.""" @property def available(self) -> bool: """Return if entity is available.""" - return self.processor.available - - async def async_added_to_hass(self) -> None: - """When entity is added to hass.""" - await super().async_added_to_hass() - self.async_on_remove( - self.processor.async_add_entity_key_listener( - self._handle_processor_update, self.entity_key - ) - ) - - @callback - def _handle_processor_update( - self, new_data: PassiveBluetoothDataUpdate | None - ) -> None: - """Handle updated data from the processor.""" - self.async_write_ha_state() + return self.coordinator.available diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py new file mode 100644 index 00000000000..2e4118000bd --- /dev/null +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -0,0 +1,346 @@ +"""Passive update processors for the Bluetooth integration.""" +from __future__ import annotations + +from collections.abc import Callable, Mapping +import dataclasses +import logging +from typing import Any, Generic, TypeVar + +from home_assistant_bluetooth import BluetoothServiceInfo + +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import BluetoothChange +from .const import DOMAIN +from .update_coordinator import BasePassiveBluetoothCoordinator + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothEntityKey: + """Key for a passive bluetooth entity. + + Example: + key: temperature + device_id: outdoor_sensor_1 + """ + + key: str + device_id: str | None + + +_T = TypeVar("_T") + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothDataUpdate(Generic[_T]): + """Generic bluetooth data.""" + + devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) + entity_descriptions: Mapping[ + PassiveBluetoothEntityKey, EntityDescription + ] = dataclasses.field(default_factory=dict) + entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field( + default_factory=dict + ) + entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field( + default_factory=dict + ) + + +class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): + """Passive bluetooth processor coordinator for bluetooth advertisements. + + The coordinator is responsible for dispatching the bluetooth data, + to each processor, and tracking devices. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + address: str, + ) -> None: + """Initialize the coordinator.""" + super().__init__(hass, logger, address) + self._processors: list[PassiveBluetoothDataProcessor] = [] + + @callback + def async_register_processor( + self, processor: PassiveBluetoothDataProcessor + ) -> Callable[[], None]: + """Register a processor that subscribes to updates.""" + processor.coordinator = self + + @callback + def remove_processor() -> None: + """Remove a processor.""" + self._processors.remove(processor) + self._async_handle_processors_changed() + + self._processors.append(processor) + self._async_handle_processors_changed() + return remove_processor + + @callback + def _async_handle_processors_changed(self) -> None: + """Handle processors changed.""" + running = bool(self._cancel_bluetooth_advertisements) + if running and not self._processors: + self._async_stop() + elif not running and self._processors: + self._async_start() + + @callback + def _async_handle_unavailable(self, address: str) -> None: + """Handle the device going unavailable.""" + super()._async_handle_unavailable(address) + for processor in self._processors: + processor.async_handle_unavailable() + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + super()._async_handle_bluetooth_event(service_info, change) + if self.hass.is_stopping: + return + for processor in self._processors: + processor.async_handle_bluetooth_event(service_info, change) + + +_PassiveBluetoothDataProcessorT = TypeVar( + "_PassiveBluetoothDataProcessorT", + bound="PassiveBluetoothDataProcessor[Any]", +) + + +class PassiveBluetoothDataProcessor(Generic[_T]): + """Passive bluetooth data processor for bluetooth advertisements. + + The processor is responsible for keeping track of the bluetooth data + and updating subscribers. + + The update_method must return a PassiveBluetoothDataUpdate object. Callers + are responsible for formatting the data returned from their parser into + the appropriate format. + + The processor will call the update_method every time the bluetooth device + receives a new advertisement data from the coordinator with the following signature: + + update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + + As the size of each advertisement is limited, the update_method should + return a PassiveBluetoothDataUpdate object that contains only data that + should be updated. The coordinator will then dispatch subscribers based + on the data in the PassiveBluetoothDataUpdate object. The accumulated data + is available in the devices, entity_data, and entity_descriptions attributes. + """ + + coordinator: PassiveBluetoothProcessorCoordinator + + def __init__( + self, + update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + ) -> None: + """Initialize the coordinator.""" + self.coordinator: PassiveBluetoothProcessorCoordinator + self._listeners: list[ + Callable[[PassiveBluetoothDataUpdate[_T] | None], None] + ] = [] + self._entity_key_listeners: dict[ + PassiveBluetoothEntityKey, + list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], + ] = {} + self.update_method = update_method + self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} + self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} + self.entity_descriptions: dict[ + PassiveBluetoothEntityKey, EntityDescription + ] = {} + self.devices: dict[str | None, DeviceInfo] = {} + self.last_update_success = True + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self.coordinator.available and self.last_update_success + + @callback + def async_handle_unavailable(self) -> None: + """Handle the device going unavailable.""" + self.async_update_listeners(None) + + @callback + def async_add_entities_listener( + self, + entity_class: type[PassiveBluetoothProcessorEntity], + async_add_entites: AddEntitiesCallback, + ) -> Callable[[], None]: + """Add a listener for new entities.""" + created: set[PassiveBluetoothEntityKey] = set() + + @callback + def _async_add_or_update_entities( + data: PassiveBluetoothDataUpdate[_T] | None, + ) -> None: + """Listen for new entities.""" + if data is None: + return + entities: list[PassiveBluetoothProcessorEntity] = [] + for entity_key, description in data.entity_descriptions.items(): + if entity_key not in created: + entities.append(entity_class(self, entity_key, description)) + created.add(entity_key) + if entities: + async_add_entites(entities) + + return self.async_add_listener(_async_add_or_update_entities) + + @callback + def async_add_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + ) -> Callable[[], None]: + """Listen for all updates.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.remove(update_callback) + + self._listeners.append(update_callback) + return remove_listener + + @callback + def async_add_entity_key_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + entity_key: PassiveBluetoothEntityKey, + ) -> Callable[[], None]: + """Listen for updates by device key.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._entity_key_listeners[entity_key].remove(update_callback) + if not self._entity_key_listeners[entity_key]: + del self._entity_key_listeners[entity_key] + + self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) + return remove_listener + + @callback + def async_update_listeners( + self, data: PassiveBluetoothDataUpdate[_T] | None + ) -> None: + """Update all registered listeners.""" + # Dispatch to listeners without a filter key + for update_callback in self._listeners: + update_callback(data) + + # Dispatch to listeners with a filter key + for listeners in self._entity_key_listeners.values(): + for update_callback in listeners: + update_callback(data) + + @callback + def async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + try: + new_data = self.update_method(service_info) + except Exception as err: # pylint: disable=broad-except + self.last_update_success = False + self.coordinator.logger.exception( + "Unexpected error updating %s data: %s", self.coordinator.name, err + ) + return + + if not isinstance(new_data, PassiveBluetoothDataUpdate): + self.last_update_success = False # type: ignore[unreachable] + raise ValueError( + f"The update_method for {self.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" + ) + + if not self.last_update_success: + self.last_update_success = True + self.coordinator.logger.info( + "Processing %s data recovered", self.coordinator.name + ) + + self.devices.update(new_data.devices) + self.entity_descriptions.update(new_data.entity_descriptions) + self.entity_data.update(new_data.entity_data) + self.entity_names.update(new_data.entity_names) + self.async_update_listeners(new_data) + + +class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]): + """A class for entities using PassiveBluetoothDataProcessor.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + processor: _PassiveBluetoothDataProcessorT, + entity_key: PassiveBluetoothEntityKey, + description: EntityDescription, + context: Any = None, + ) -> None: + """Create the entity with a PassiveBluetoothDataProcessor.""" + self.entity_description = description + self.entity_key = entity_key + self.processor = processor + self.processor_context = context + address = processor.coordinator.address + device_id = entity_key.device_id + devices = processor.devices + key = entity_key.key + if device_id in devices: + base_device_info = devices[device_id] + else: + base_device_info = DeviceInfo({}) + if device_id: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}} + ) + self._attr_unique_id = f"{address}-{key}-{device_id}" + else: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, address)}} + ) + self._attr_unique_id = f"{address}-{key}" + if ATTR_NAME not in self._attr_device_info: + self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name + self._attr_name = processor.entity_names.get(entity_key) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.processor.available + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.processor.async_add_entity_key_listener( + self._handle_processor_update, self.entity_key + ) + ) + + @callback + def _handle_processor_update( + self, new_data: PassiveBluetoothDataUpdate | None + ) -> None: + """Handle updated data from the processor.""" + self.async_write_ha_state() diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py new file mode 100644 index 00000000000..d45514ab9ab --- /dev/null +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -0,0 +1,84 @@ +"""Update coordinator for the Bluetooth integration.""" +from __future__ import annotations + +import logging +import time + +from home_assistant_bluetooth import BluetoothServiceInfo + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback + +from . import ( + BluetoothCallbackMatcher, + BluetoothChange, + async_register_callback, + async_track_unavailable, +) + + +class BasePassiveBluetoothCoordinator: + """Base class for passive bluetooth coordinator for bluetooth advertisements. + + The coordinator is responsible for tracking devices. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + address: str, + ) -> None: + """Initialize the coordinator.""" + self.hass = hass + self.logger = logger + self.name: str | None = None + self.address = address + self._cancel_track_unavailable: CALLBACK_TYPE | None = None + self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None + self._present = False + self.last_seen = 0.0 + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._present + + @callback + def _async_start(self) -> None: + """Start the callbacks.""" + self._cancel_bluetooth_advertisements = async_register_callback( + self.hass, + self._async_handle_bluetooth_event, + BluetoothCallbackMatcher(address=self.address), + ) + self._cancel_track_unavailable = async_track_unavailable( + self.hass, + self._async_handle_unavailable, + self.address, + ) + + @callback + def _async_stop(self) -> None: + """Stop the callbacks.""" + if self._cancel_bluetooth_advertisements is not None: + self._cancel_bluetooth_advertisements() + self._cancel_bluetooth_advertisements = None + if self._cancel_track_unavailable is not None: + self._cancel_track_unavailable() + self._cancel_track_unavailable = None + + @callback + def _async_handle_unavailable(self, address: str) -> None: + """Handle the device going unavailable.""" + self._present = False + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.last_seen = time.monotonic() + self.name = service_info.name + self._present = True diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 330d85b665c..574f2a23355 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,8 +3,8 @@ from __future__ import annotations import logging -from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdateCoordinator, +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert address is not None hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothDataUpdateCoordinator( + ] = PassiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 600f28b6880..6b472edefe9 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -13,11 +13,11 @@ from inkbird_ble import ( ) from homeassistant import config_entries -from homeassistant.components.bluetooth.passive_update_coordinator import ( +from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -126,7 +126,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the INKBIRD BLE sensors.""" - coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] data = INKBIRDBluetoothDeviceData() diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 94e98e30f82..0f0d4d4fe8f 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,8 +3,8 @@ from __future__ import annotations import logging -from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdateCoordinator, +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert address is not None hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothDataUpdateCoordinator( + ] = PassiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index f592b594289..3921cbe43dd 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -13,11 +13,11 @@ from sensorpush_ble import ( ) from homeassistant import config_entries -from homeassistant.components.bluetooth.passive_update_coordinator import ( +from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -127,7 +127,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the SensorPush BLE sensors.""" - coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] data = SensorPushBluetoothDeviceData() diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index d7f3e4071aa..a40dc8995d1 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,8 +3,8 @@ from __future__ import annotations import logging -from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdateCoordinator, +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert address is not None hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothDataUpdateCoordinator( + ] = PassiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 1452b5e9053..50cbb0f66cf 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -13,11 +13,11 @@ from xiaomi_ble import ( ) from homeassistant import config_entries -from homeassistant.components.bluetooth.passive_update_coordinator import ( +from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -155,7 +155,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi BLE sensors.""" - coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] data = XiaomiBluetoothDeviceData() diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 2b8c876460f..4da14fb13d3 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -1,39 +1,27 @@ -"""Tests for the Bluetooth integration.""" +"""Tests for the Bluetooth integration PassiveBluetoothDataUpdateCoordinator.""" from __future__ import annotations from datetime import timedelta import logging +from typing import Any from unittest.mock import MagicMock, patch -from home_assistant_bluetooth import BluetoothServiceInfo -import pytest - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntityDescription, -) from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataProcessor, - PassiveBluetoothDataUpdate, + PassiveBluetoothCoordinatorEntity, PassiveBluetoothDataUpdateCoordinator, - PassiveBluetoothEntityKey, - PassiveBluetoothProcessorEntity, ) -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.const import TEMP_CELSIUS -from homeassistant.core import CoreState, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import _get_underlying_scanner -from tests.common import MockEntityPlatform, async_fire_time_changed +from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -49,49 +37,30 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="local", ) -GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={ - None: DeviceInfo( - name="Test Device", model="Test Model", manufacturer="Test Manufacturer" - ), - }, - entity_data={ - PassiveBluetoothEntityKey("temperature", None): 14.5, - PassiveBluetoothEntityKey("pressure", None): 1234, - }, - entity_names={ - PassiveBluetoothEntityKey("temperature", None): "Temperature", - PassiveBluetoothEntityKey("pressure", None): "Pressure", - }, - entity_descriptions={ - PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( - key="temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( - key="pressure", - native_unit_of_measurement="hPa", - device_class=SensorDeviceClass.PRESSURE, - ), - }, -) + + +class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): + """An example coordinator that subclasses PassiveBluetoothDataUpdateCoordinator.""" + + def __init__(self, hass, logger, device_id) -> None: + """Initialize the coordinator.""" + super().__init__(hass, logger, device_id) + self.data: dict[str, Any] = {} + + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.data = {"rssi": service_info.rssi} + super()._async_handle_bluetooth_event(service_info, change) async def test_basic_usage(hass, mock_bleak_scanner_start): """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) + coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") assert coordinator.available is False # no data yet saved_callback = None @@ -100,98 +69,87 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + mock_listener = MagicMock() + unregister_listener = coordinator.async_add_listener(mock_listener) with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): - unregister_processor = coordinator.async_register_processor(processor) + cancel = coordinator.async_start() - entity_key = PassiveBluetoothEntityKey("temperature", None) - entity_key_events = [] - all_events = [] - mock_entity = MagicMock() - mock_add_entities = MagicMock() - - def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock entity key listener.""" - entity_key_events.append(data) - - cancel_async_add_entity_key_listener = processor.async_add_entity_key_listener( - _async_entity_key_listener, - entity_key, - ) - - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) - - cancel_listener = processor.async_add_listener( - _all_listener, - ) - - cancel_async_add_entities_listener = processor.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + assert saved_callback is not None saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Each listener should receive the same data - # since both match - assert len(entity_key_events) == 1 - assert len(all_events) == 1 - - # There should be 4 calls to create entities - assert len(mock_entity.mock_calls) == 2 - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - - # Each listener should receive the same data - # since both match - assert len(entity_key_events) == 2 - assert len(all_events) == 2 - - # On the second, the entities should already be created - # so the mock should not be called again - assert len(mock_entity.mock_calls) == 2 - - cancel_async_add_entity_key_listener() - cancel_listener() - cancel_async_add_entities_listener() - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - - # Each listener should not trigger any more now - # that they were cancelled - assert len(entity_key_events) == 2 - assert len(all_events) == 2 - assert len(mock_entity.mock_calls) == 2 + assert len(mock_listener.mock_calls) == 1 + assert coordinator.data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} assert coordinator.available is True - unregister_processor() + unregister_listener() + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + assert len(mock_listener.mock_calls) == 1 + assert coordinator.data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} + assert coordinator.available is True + cancel() -async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): - """Test that the coordinator is unavailable after no data for a while.""" +async def test_context_compatiblity_with_data_update_coordinator( + hass, mock_bleak_scanner_start +): + """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + mock_listener = MagicMock() + coordinator.async_add_listener(mock_listener) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_start() + + assert not set(coordinator.async_contexts()) + + def update_callback1(): + pass + + def update_callback2(): + pass + + unsub1 = coordinator.async_add_listener(update_callback1, 1) + assert set(coordinator.async_contexts()) == {1} + + unsub2 = coordinator.async_add_listener(update_callback2, 2) + assert set(coordinator.async_contexts()) == {1, 2} + + unsub1() + assert set(coordinator.async_contexts()) == {2} + + unsub2() + assert not set(coordinator.async_contexts()) + + +async def test_unavailable_callbacks_mark_the_coordinator_unavailable( + hass, mock_bleak_scanner_start +): + """Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device.""" with patch( "bleak.BleakScanner.discovered_devices", # Must patch before we setup [MagicMock(address="44:44:33:11:23:45")], ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) + coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") assert coordinator.available is False # no data yet saved_callback = None @@ -200,27 +158,19 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + mock_listener = MagicMock() + coordinator.async_add_listener(mock_listener) + with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): - unregister_processor = coordinator.async_register_processor(processor) - - mock_entity = MagicMock() - mock_add_entities = MagicMock() - processor.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + coordinator.async_start() assert coordinator.available is False - assert processor.available is False - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - assert processor.available is True + scanner = _get_underlying_scanner() with patch( @@ -236,12 +186,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): ) await hass.async_block_till_done() assert coordinator.available is False - assert processor.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - assert processor.available is True with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", @@ -256,474 +203,15 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): ) await hass.async_block_till_done() assert coordinator.available is False - assert processor.available is False - - unregister_processor() -async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): - """Test updates are ignored once hass is stopping.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - - all_events = [] - - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) - - processor.async_add_listener( - _all_listener, - ) - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert len(all_events) == 1 - - hass.state = CoreState.stopping - - # We should stop processing events once hass is stopping - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert len(all_events) == 1 - unregister_processor() - - -async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): - """Test we handle exceptions from the update method.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - run_count = 0 - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - nonlocal run_count - run_count += 1 - if run_count == 2: - raise Exception("Test exception") - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - - processor.async_add_listener(MagicMock()) - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert processor.available is True - - # We should go unavailable once we get an exception - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert "Test exception" in caplog.text - assert processor.available is False - - # We should go available again once we get data again - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert processor.available is True - unregister_processor() - - -async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): - """Test we handle bad data from the update method.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - run_count = 0 - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - nonlocal run_count - run_count += 1 - if run_count == 2: - return "bad_data" - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - - processor.async_add_listener(MagicMock()) - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert processor.available is True - - # We should go unavailable once we get bad data - with pytest.raises(ValueError): - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - - assert processor.available is False - - # We should go available again once we get good data again - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert processor.available is True - unregister_processor() - - -GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( - name="B5178D6FB", - address="749A17CB-F7A9-D466-C29F-AABE601938A0", - rssi=-95, - manufacturer_data={ - 1: b"\x01\x01\x01\x04\xb5\xa2d\x00\x06L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" - }, - service_data={}, - service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], - source="local", -) -GOVEE_B5178_PRIMARY_SERVICE_INFO = BluetoothServiceInfo( - name="B5178D6FB", - address="749A17CB-F7A9-D466-C29F-AABE601938A0", - rssi=-92, - manufacturer_data={ - 1: b"\x01\x01\x00\x03\x07Xd\x00\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" - }, - service_data={}, - service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], - source="local", -) - -GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={ - "remote": { - "name": "B5178D6FB Remote", - "manufacturer": "Govee", - "model": "H5178-REMOTE", - }, - }, - entity_descriptions={ - PassiveBluetoothEntityKey( - key="temperature", device_id="remote" - ): SensorEntityDescription( - key="temperature_remote", - device_class=SensorDeviceClass.TEMPERATURE, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="°C", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="humidity", device_id="remote" - ): SensorEntityDescription( - key="humidity_remote", - device_class=SensorDeviceClass.HUMIDITY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="battery", device_id="remote" - ): SensorEntityDescription( - key="battery_remote", - device_class=SensorDeviceClass.BATTERY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="signal_strength", device_id="remote" - ): SensorEntityDescription( - key="signal_strength_remote", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=None, - entity_registry_enabled_default=False, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="dBm", - state_class=None, - ), - }, - entity_names={ - PassiveBluetoothEntityKey(key="temperature", device_id="remote"): "Temperature", - PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", - PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", - PassiveBluetoothEntityKey( - key="signal_strength", device_id="remote" - ): "Signal Strength", - }, - entity_data={ - PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, - PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, - PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, - PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -95, - }, -) -GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( - PassiveBluetoothDataUpdate( - devices={ - "remote": { - "name": "B5178D6FB Remote", - "manufacturer": "Govee", - "model": "H5178-REMOTE", - }, - "primary": { - "name": "B5178D6FB Primary", - "manufacturer": "Govee", - "model": "H5178", - }, - }, - entity_descriptions={ - PassiveBluetoothEntityKey( - key="temperature", device_id="remote" - ): SensorEntityDescription( - key="temperature_remote", - device_class=SensorDeviceClass.TEMPERATURE, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="°C", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="humidity", device_id="remote" - ): SensorEntityDescription( - key="humidity_remote", - device_class=SensorDeviceClass.HUMIDITY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="battery", device_id="remote" - ): SensorEntityDescription( - key="battery_remote", - device_class=SensorDeviceClass.BATTERY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="signal_strength", device_id="remote" - ): SensorEntityDescription( - key="signal_strength_remote", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=None, - entity_registry_enabled_default=False, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="dBm", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="temperature", device_id="primary" - ): SensorEntityDescription( - key="temperature_primary", - device_class=SensorDeviceClass.TEMPERATURE, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="°C", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="humidity", device_id="primary" - ): SensorEntityDescription( - key="humidity_primary", - device_class=SensorDeviceClass.HUMIDITY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="battery", device_id="primary" - ): SensorEntityDescription( - key="battery_primary", - device_class=SensorDeviceClass.BATTERY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="signal_strength", device_id="primary" - ): SensorEntityDescription( - key="signal_strength_primary", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=None, - entity_registry_enabled_default=False, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="dBm", - state_class=None, - ), - }, - entity_names={ - PassiveBluetoothEntityKey( - key="temperature", device_id="remote" - ): "Temperature", - PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", - PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", - PassiveBluetoothEntityKey( - key="signal_strength", device_id="remote" - ): "Signal Strength", - PassiveBluetoothEntityKey( - key="temperature", device_id="primary" - ): "Temperature", - PassiveBluetoothEntityKey(key="humidity", device_id="primary"): "Humidity", - PassiveBluetoothEntityKey(key="battery", device_id="primary"): "Battery", - PassiveBluetoothEntityKey( - key="signal_strength", device_id="primary" - ): "Signal Strength", - }, - entity_data={ - PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, - PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, - PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, - PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -92, - PassiveBluetoothEntityKey(key="temperature", device_id="primary"): 19.8488, - PassiveBluetoothEntityKey(key="humidity", device_id="primary"): 48.8, - PassiveBluetoothEntityKey(key="battery", device_id="primary"): 100, - PassiveBluetoothEntityKey(key="signal_strength", device_id="primary"): -92, - }, - ) -) - - -async def test_integration_with_entity(hass, mock_bleak_scanner_start): +async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start): """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + entity = PassiveBluetoothCoordinatorEntity(coordinator) + assert entity.available is False - update_count = 0 - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - nonlocal update_count - update_count += 1 - if update_count > 2: - return GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE - return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet saved_callback = None def _async_register_callback(_hass, _callback, _matcher): @@ -731,328 +219,15 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_register_processor(processor) - - processor.async_add_listener(MagicMock()) - - mock_add_entities = MagicMock() - - processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - mock_add_entities, - ) + coordinator.async_start() + assert coordinator.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # First call with just the remote sensor entities results in them being added - assert len(mock_add_entities.mock_calls) == 1 - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Second call with just the remote sensor entities does not add them again - assert len(mock_add_entities.mock_calls) == 1 - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Third call with primary and remote sensor entities adds the primary sensor entities - assert len(mock_add_entities.mock_calls) == 2 - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Forth call with both primary and remote sensor entities does not add them again - assert len(mock_add_entities.mock_calls) == 2 - - entities = [ - *mock_add_entities.mock_calls[0][1][0], - *mock_add_entities.mock_calls[1][1][0], - ] - - entity_one: PassiveBluetoothProcessorEntity = entities[0] - entity_one.hass = hass - assert entity_one.available is True - assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote" - assert entity_one.device_info == { - "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff-remote")}, - "manufacturer": "Govee", - "model": "H5178-REMOTE", - "name": "B5178D6FB Remote", - } - assert entity_one.entity_key == PassiveBluetoothEntityKey( - key="temperature", device_id="remote" - ) - - -NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( - name="Generic", - address="aa:bb:cc:dd:ee:ff", - rssi=-95, - manufacturer_data={ - 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", - }, - service_data={}, - service_uuids=[], - source="local", -) -NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={}, - entity_data={ - PassiveBluetoothEntityKey("temperature", None): 14.5, - PassiveBluetoothEntityKey("pressure", None): 1234, - }, - entity_descriptions={ - PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( - key="temperature", - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( - key="pressure", - name="Pressure", - native_unit_of_measurement="hPa", - device_class=SensorDeviceClass.PRESSURE, - ), - }, -) - - -async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start): - """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(processor) - - mock_add_entities = MagicMock() - - processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - mock_add_entities, - ) - - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # First call with just the remote sensor entities results in them being added - assert len(mock_add_entities.mock_calls) == 1 - - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Second call with just the remote sensor entities does not add them again - assert len(mock_add_entities.mock_calls) == 1 - - entities = mock_add_entities.mock_calls[0][1][0] - entity_one: PassiveBluetoothProcessorEntity = entities[0] - entity_one.hass = hass - assert entity_one.available is True - assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" - assert entity_one.device_info == { - "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, - "name": "Generic", - } - assert entity_one.entity_key == PassiveBluetoothEntityKey( - key="temperature", device_id=None - ) - - -async def test_passive_bluetooth_entity_with_entity_platform( - hass, mock_bleak_scanner_start -): - """Test with a mock entity platform.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - entity_platform = MockEntityPlatform(hass) - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(processor) - - processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - lambda entities: hass.async_create_task( - entity_platform.async_add_entities(entities) - ), - ) - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - await hass.async_block_till_done() - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - await hass.async_block_till_done() - assert ( - hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") - is not None - ) - assert ( - hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") - is not None - ) - - -SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={ - None: DeviceInfo( - name="Test Device", model="Test Model", manufacturer="Test Manufacturer" - ), - }, - entity_data={ - PassiveBluetoothEntityKey("pressure", None): 1234, - }, - entity_names={ - PassiveBluetoothEntityKey("pressure", None): "Pressure", - }, - entity_descriptions={ - PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( - key="pressure", - native_unit_of_measurement="hPa", - device_class=SensorDeviceClass.PRESSURE, - ), - }, -) - - -BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={ - None: DeviceInfo( - name="Test Device", model="Test Model", manufacturer="Test Manufacturer" - ), - }, - entity_data={ - PassiveBluetoothEntityKey("motion", None): True, - }, - entity_names={ - PassiveBluetoothEntityKey("motion", None): "Motion", - }, - entity_descriptions={ - PassiveBluetoothEntityKey("motion", None): BinarySensorEntityDescription( - key="motion", - device_class=BinarySensorDeviceClass.MOTION, - ), - }, -) - - -async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start): - """Test integration of PassiveBluetoothDataUpdateCoordinator with multiple platforms.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - binary_sensor_processor = PassiveBluetoothDataProcessor( - lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE - ) - sesnor_processor = PassiveBluetoothDataProcessor( - lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE - ) - - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(binary_sensor_processor) - coordinator.async_register_processor(sesnor_processor) - - binary_sensor_processor.async_add_listener(MagicMock()) - sesnor_processor.async_add_listener(MagicMock()) - - mock_add_sensor_entities = MagicMock() - mock_add_binary_sensor_entities = MagicMock() - - sesnor_processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - mock_add_sensor_entities, - ) - binary_sensor_processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - mock_add_binary_sensor_entities, - ) - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # First call with just the remote sensor entities results in them being added - assert len(mock_add_binary_sensor_entities.mock_calls) == 1 - assert len(mock_add_sensor_entities.mock_calls) == 1 - - binary_sesnor_entities = [ - *mock_add_binary_sensor_entities.mock_calls[0][1][0], - ] - sesnor_entities = [ - *mock_add_sensor_entities.mock_calls[0][1][0], - ] - - sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_entities[0] - sensor_entity_one.hass = hass - assert sensor_entity_one.available is True - assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" - assert sensor_entity_one.device_info == { - "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, - "manufacturer": "Test Manufacturer", - "model": "Test Model", - "name": "Test Device", - } - assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( - key="pressure", device_id=None - ) - - binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sesnor_entities[ - 0 - ] - binary_sensor_entity_one.hass = hass - assert binary_sensor_entity_one.available is True - assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" - assert binary_sensor_entity_one.device_info == { - "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, - "manufacturer": "Test Manufacturer", - "model": "Test Model", - "name": "Test Device", - } - assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( - key="motion", device_id=None - ) + assert coordinator.available is True + entity.hass = hass + await entity.async_update() + assert entity.available is True diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py new file mode 100644 index 00000000000..e5f992eebb2 --- /dev/null +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -0,0 +1,1058 @@ +"""Tests for the Bluetooth integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from unittest.mock import MagicMock, patch + +from home_assistant_bluetooth import BluetoothServiceInfo +import pytest + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth import ( + DOMAIN, + UNAVAILABLE_TRACK_SECONDS, + BluetoothChange, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import CoreState, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import _get_underlying_scanner + +from tests.common import MockEntityPlatform, async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_names={ + PassiveBluetoothEntityKey("temperature", None): "Temperature", + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_basic_usage(hass, mock_bleak_scanner_start): + """Test basic usage of the PassiveBluetoothProcessorCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + entity_key = PassiveBluetoothEntityKey("temperature", None) + entity_key_events = [] + all_events = [] + mock_entity = MagicMock() + mock_add_entities = MagicMock() + + def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock entity key listener.""" + entity_key_events.append(data) + + cancel_async_add_entity_key_listener = processor.async_add_entity_key_listener( + _async_entity_key_listener, + entity_key, + ) + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + cancel_listener = processor.async_add_listener( + _all_listener, + ) + + cancel_async_add_entities_listener = processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 1 + assert len(all_events) == 1 + + # There should be 4 calls to create entities + assert len(mock_entity.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + + # On the second, the entities should already be created + # so the mock should not be called again + assert len(mock_entity.mock_calls) == 2 + + cancel_async_add_entity_key_listener() + cancel_listener() + cancel_async_add_entities_listener() + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should not trigger any more now + # that they were cancelled + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + assert len(mock_entity.mock_calls) == 2 + assert coordinator.available is True + + unregister_processor() + + +async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): + """Test that the coordinator is unavailable after no data for a while.""" + with patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + mock_entity = MagicMock() + mock_add_entities = MagicMock() + processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + assert coordinator.available is False + assert processor.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 + assert coordinator.available is True + assert processor.available is True + scanner = _get_underlying_scanner() + + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch.object( + scanner, + "history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert coordinator.available is False + assert processor.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 + assert coordinator.available is True + assert processor.available is True + + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch.object( + scanner, + "history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert coordinator.available is False + assert processor.available is False + + unregister_processor() + + +async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): + """Test updates are ignored once hass is stopping.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + all_events = [] + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + processor.async_add_listener( + _all_listener, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + + hass.state = CoreState.stopping + + # We should stop processing events once hass is stopping + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + unregister_processor() + + +async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): + """Test we handle exceptions from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + raise Exception("Test exception") + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + + # We should go unavailable once we get an exception + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert "Test exception" in caplog.text + assert processor.available is False + + # We should go available again once we get data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + unregister_processor() + + +async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): + """Test we handle bad data from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + return "bad_data" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + + # We should go unavailable once we get bad data + with pytest.raises(ValueError): + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + assert processor.available is False + + # We should go available again once we get good data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + unregister_processor() + + +GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x04\xb5\xa2d\x00\x06L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) +GOVEE_B5178_PRIMARY_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-92, + manufacturer_data={ + 1: b"\x01\x01\x00\x03\x07Xd\x00\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) + +GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_names={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -95, + }, +) +GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( + PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + "primary": { + "name": "B5178D6FB Primary", + "manufacturer": "Govee", + "model": "H5178", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): SensorEntityDescription( + key="temperature_primary", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="primary" + ): SensorEntityDescription( + key="humidity_primary", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="primary" + ): SensorEntityDescription( + key="battery_primary", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): SensorEntityDescription( + key="signal_strength_primary", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_names={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="primary"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): "Signal Strength", + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -92, + PassiveBluetoothEntityKey(key="temperature", device_id="primary"): 19.8488, + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): 48.8, + PassiveBluetoothEntityKey(key="battery", device_id="primary"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="primary"): -92, + }, + ) +) + + +async def test_integration_with_entity(hass, mock_bleak_scanner_start): + """Test integration of PassiveBluetoothProcessorCoordinator with PassiveBluetoothCoordinatorEntity.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + update_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal update_count + update_count += 1 + if update_count > 2: + return GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) + + mock_add_entities = MagicMock() + + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Third call with primary and remote sensor entities adds the primary sensor entities + assert len(mock_add_entities.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Forth call with both primary and remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 2 + + entities = [ + *mock_add_entities.mock_calls[0][1][0], + *mock_add_entities.mock_calls[1][1][0], + ] + + entity_one: PassiveBluetoothProcessorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff-remote")}, + "manufacturer": "Govee", + "model": "H5178-REMOTE", + "name": "B5178D6FB Remote", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ) + + +NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={}, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + name="Pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start): + """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(processor) + + mock_add_entities = MagicMock() + + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_entities, + ) + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + entities = mock_add_entities.mock_calls[0][1][0] + entity_one: PassiveBluetoothProcessorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "name": "Generic", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id=None + ) + + +async def test_passive_bluetooth_entity_with_entity_platform( + hass, mock_bleak_scanner_start +): + """Test with a mock entity platform.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + entity_platform = MockEntityPlatform(hass) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(processor) + + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + lambda entities: hass.async_create_task( + entity_platform.async_add_entities(entities) + ), + ) + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") + is not None + ) + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") + is not None + ) + + +SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_names={ + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("motion", None): True, + }, + entity_names={ + PassiveBluetoothEntityKey("motion", None): "Motion", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("motion", None): BinarySensorEntityDescription( + key="motion", + device_class=BinarySensorDeviceClass.MOTION, + ), + }, +) + + +async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start): + """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + binary_sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + sesnor_processor = PassiveBluetoothDataProcessor( + lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(binary_sensor_processor) + coordinator.async_register_processor(sesnor_processor) + + binary_sensor_processor.async_add_listener(MagicMock()) + sesnor_processor.async_add_listener(MagicMock()) + + mock_add_sensor_entities = MagicMock() + mock_add_binary_sensor_entities = MagicMock() + + sesnor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_sensor_entities, + ) + binary_sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_binary_sensor_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_binary_sensor_entities.mock_calls) == 1 + assert len(mock_add_sensor_entities.mock_calls) == 1 + + binary_sesnor_entities = [ + *mock_add_binary_sensor_entities.mock_calls[0][1][0], + ] + sesnor_entities = [ + *mock_add_sensor_entities.mock_calls[0][1][0], + ] + + sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_entities[0] + sensor_entity_one.hass = hass + assert sensor_entity_one.available is True + assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" + assert sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="pressure", device_id=None + ) + + binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sesnor_entities[ + 0 + ] + binary_sensor_entity_one.hass = hass + assert binary_sensor_entity_one.available is True + assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" + assert binary_sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="motion", device_id=None + ) diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index 80a2179666f..a851cb92ec3 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -28,7 +28,7 @@ async def test_sensors(hass): return lambda: None with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index c48b8bc3407..31fbdd8d712 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -28,7 +28,7 @@ async def test_sensors(hass): return lambda: None with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 44b29ff1051..74a4fe65131 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -28,7 +28,7 @@ async def test_sensors(hass): return lambda: None with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): assert await hass.config_entries.async_setup(entry.entry_id) @@ -66,7 +66,7 @@ async def test_xiaomi_HHCCJCY01(hass): return lambda: None with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): assert await hass.config_entries.async_setup(entry.entry_id) From a499dfb8ff4eabe00aa06d2e9c5668f913f97cb8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 23 Jul 2022 20:06:10 -0600 Subject: [PATCH 657/820] Fix AssertionError in RainMachine (#75668) --- homeassistant/components/rainmachine/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 5a5329ad1fa..e57386fe0ec 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -243,10 +243,8 @@ class TimeRemainingSensor(RainMachineEntity, RestoreSensor): seconds_remaining = self.calculate_seconds_remaining() new_timestamp = now + timedelta(seconds=seconds_remaining) - assert isinstance(self._attr_native_value, datetime) - if ( - self._attr_native_value + isinstance(self._attr_native_value, datetime) and new_timestamp - self._attr_native_value < DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE ): From 82c92b56348423e082ac742683eb40f60d9396b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 02:51:14 -0500 Subject: [PATCH 658/820] Add Moat (BLE) integration (#75643) * Add Moat (BLE) integration * fix pin * resync * Update tests/components/moat/test_sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/moat/sensor.py * backmerge from integration * purge dead code Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 + homeassistant/components/moat/__init__.py | 40 +++++ homeassistant/components/moat/config_flow.py | 93 ++++++++++ homeassistant/components/moat/const.py | 3 + homeassistant/components/moat/manifest.json | 11 ++ homeassistant/components/moat/sensor.py | 164 ++++++++++++++++++ homeassistant/components/moat/strings.json | 21 +++ .../components/moat/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/moat/__init__.py | 32 ++++ tests/components/moat/conftest.py | 8 + tests/components/moat/test_config_flow.py | 164 ++++++++++++++++++ tests/components/moat/test_sensor.py | 50 ++++++ 16 files changed, 620 insertions(+) create mode 100644 homeassistant/components/moat/__init__.py create mode 100644 homeassistant/components/moat/config_flow.py create mode 100644 homeassistant/components/moat/const.py create mode 100644 homeassistant/components/moat/manifest.json create mode 100644 homeassistant/components/moat/sensor.py create mode 100644 homeassistant/components/moat/strings.json create mode 100644 homeassistant/components/moat/translations/en.json create mode 100644 tests/components/moat/__init__.py create mode 100644 tests/components/moat/conftest.py create mode 100644 tests/components/moat/test_config_flow.py create mode 100644 tests/components/moat/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 073170795a1..c5bfb0cf24c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -648,6 +648,8 @@ build.json @home-assistant/supervisor /tests/components/minecraft_server/ @elmurato /homeassistant/components/minio/ @tkislan /tests/components/minio/ @tkislan +/homeassistant/components/moat/ @bdraco +/tests/components/moat/ @bdraco /homeassistant/components/mobile_app/ @home-assistant/core /tests/components/mobile_app/ @home-assistant/core /homeassistant/components/modbus/ @adamchengtkc @janiversen @vzahradnik diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py new file mode 100644 index 00000000000..bd32ef64a0f --- /dev/null +++ b/homeassistant/components/moat/__init__.py @@ -0,0 +1,40 @@ +"""The Moat Bluetooth BLE integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Moat BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py new file mode 100644 index 00000000000..a353f4963ad --- /dev/null +++ b/homeassistant/components/moat/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for moat ble integration.""" +from __future__ import annotations + +from typing import Any + +from moat_ble import MoatBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class MoatConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for moat.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/moat/const.py b/homeassistant/components/moat/const.py new file mode 100644 index 00000000000..c2975343758 --- /dev/null +++ b/homeassistant/components/moat/const.py @@ -0,0 +1,3 @@ +"""Constants for the Moat Bluetooth integration.""" + +DOMAIN = "moat" diff --git a/homeassistant/components/moat/manifest.json b/homeassistant/components/moat/manifest.json new file mode 100644 index 00000000000..49e6985d1c1 --- /dev/null +++ b/homeassistant/components/moat/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "moat", + "name": "Moat", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/moat", + "bluetooth": [{ "local_name": "Moat_S*" }], + "requirements": ["moat-ble==0.1.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py new file mode 100644 index 00000000000..f29111a3406 --- /dev/null +++ b/homeassistant/components/moat/sensor.py @@ -0,0 +1,164 @@ +"""Support for moat ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from moat_ble import ( + DeviceClass, + DeviceKey, + MoatBluetoothDeviceData, + SensorDeviceInfo, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ELECTRIC_POTENTIAL_VOLT, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( + key=f"{DeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_VOLT}", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to hass device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Moat BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + data = MoatBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + processor.async_add_entities_listener( + MoatBluetoothSensorEntity, async_add_entities + ) + ) + + +class MoatBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a moat ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/moat/strings.json b/homeassistant/components/moat/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/moat/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/moat/translations/en.json b/homeassistant/components/moat/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/moat/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 175b82dbcfd..4e527a02bc0 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -30,6 +30,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "inkbird", "local_name": "tps" }, + { + "domain": "moat", + "local_name": "Moat_S*" + }, { "domain": "sensorpush", "local_name": "SensorPush*" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 917eca321ea..33bd4ea0a52 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -217,6 +217,7 @@ FLOWS = { "mill", "minecraft_server", "mjpeg", + "moat", "mobile_app", "modem_callerid", "modern_forms", diff --git a/requirements_all.txt b/requirements_all.txt index 4309d2f34c5..e0f3abff185 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1049,6 +1049,9 @@ minio==5.0.10 # homeassistant.components.mitemp_bt mitemp_bt==0.0.5 +# homeassistant.components.moat +moat-ble==0.1.1 + # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d8e790b50f..ee41c383f4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -738,6 +738,9 @@ millheater==0.9.0 # homeassistant.components.minio minio==5.0.10 +# homeassistant.components.moat +moat-ble==0.1.1 + # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.2.1 diff --git a/tests/components/moat/__init__.py b/tests/components/moat/__init__.py new file mode 100644 index 00000000000..358d338993f --- /dev/null +++ b/tests/components/moat/__init__.py @@ -0,0 +1,32 @@ +"""Tests for the Moat BLE integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_MOAT_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +MOAT_S2_SERVICE_INFO = BluetoothServiceInfo( + name="Moat_S2", + manufacturer_data={}, + service_data={ + "00005000-0000-1000-8000-00805f9b34fb": b"\xdfy\xe3\xa6\x12\xb3\xf5\x0b", + "00001000-0000-1000-8000-00805f9b34fb": ( + b"\xdfy\xe3\xa6\x12\xb3\x11S\xdbb\xfcbpq" b"\xf5\x0b\xff\xff" + ), + }, + service_uuids=[ + "00001000-0000-1000-8000-00805f9b34fb", + "00002000-0000-1000-8000-00805f9b34fb", + ], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + source="local", +) diff --git a/tests/components/moat/conftest.py b/tests/components/moat/conftest.py new file mode 100644 index 00000000000..1f7f00c8d2f --- /dev/null +++ b/tests/components/moat/conftest.py @@ -0,0 +1,8 @@ +"""Moat session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/moat/test_config_flow.py b/tests/components/moat/test_config_flow.py new file mode 100644 index 00000000000..7ceeb2ad73f --- /dev/null +++ b/tests/components/moat/test_config_flow.py @@ -0,0 +1,164 @@ +"""Test the Moat config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.moat.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import MOAT_S2_SERVICE_INFO, NOT_MOAT_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Moat S2 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_moat(hass): + """Test discovery via bluetooth not moat.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_MOAT_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Moat S2 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Moat S2 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/moat/test_sensor.py b/tests/components/moat/test_sensor.py new file mode 100644 index 00000000000..826bbc72cdb --- /dev/null +++ b/tests/components/moat/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Moat sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.moat.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import MOAT_S2_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(MOAT_S2_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + temp_sensor = hass.states.get("sensor.moat_s2_eeff_voltage") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "3.061" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Moat S2 EEFF Voltage" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "V" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 7075032bf743f8702d942410c0c41214c90c212b Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 24 Jul 2022 09:21:01 +0100 Subject: [PATCH 659/820] Fix diagnostics export for generic camera (#75665) Fix url redaction and add tests Co-authored-by: Dave T --- .../components/generic/diagnostics.py | 6 ++-- tests/components/generic/test_diagnostics.py | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic/diagnostics.py b/homeassistant/components/generic/diagnostics.py index 00be287f053..39d6a81ad88 100644 --- a/homeassistant/components/generic/diagnostics.py +++ b/homeassistant/components/generic/diagnostics.py @@ -21,12 +21,12 @@ TO_REDACT = { # A very similar redact function is in components.sql. Possible to be made common. def redact_url(data: str) -> str: """Redact credentials from string url.""" - url_in = yarl.URL(data) + url = url_in = yarl.URL(data) if url_in.user: - url = url_in.with_user("****") + url = url.with_user("****") if url_in.password: url = url.with_password("****") - if url_in.path: + if url_in.path != "/": url = url.with_path("****") if url_in.query_string: url = url.with_query("****=****") diff --git a/tests/components/generic/test_diagnostics.py b/tests/components/generic/test_diagnostics.py index 2d4e4c536d8..d31503c11c8 100644 --- a/tests/components/generic/test_diagnostics.py +++ b/tests/components/generic/test_diagnostics.py @@ -1,5 +1,8 @@ """Test generic (IP camera) diagnostics.""" +import pytest + from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.generic.diagnostics import redact_url from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -22,3 +25,34 @@ async def test_entry_diagnostics(hass, hass_client, setup_entry): "content_type": "image/jpeg", }, } + + +@pytest.mark.parametrize( + ("url_in", "url_out_expected"), + [ + ( + "http://www.example.com", + "http://www.example.com", + ), + ( + "http://fred:letmein1@www.example.com/image.php?key=secret2", + "http://****:****@www.example.com/****?****=****", + ), + ( + "http://fred@www.example.com/image.php?key=secret2", + "http://****@www.example.com/****?****=****", + ), + ( + "http://fred@www.example.com/image.php", + "http://****@www.example.com/****", + ), + ( + "http://:letmein1@www.example.com", + "http://:****@www.example.com", + ), + ], +) +def test_redact_url(url_in, url_out_expected): + """Test url redaction.""" + url_out = redact_url(url_in) + assert url_out == url_out_expected From ba71a3c24dbff4ebee2dd7a7fbd905d98fb8c7e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 03:39:23 -0500 Subject: [PATCH 660/820] Add Govee BLE integration (#75631) * Add Govee BLE integration * add missing files * remove test file not needed yet * fix * add bbq sensors * fixed lib * bump again to fix the names * fix discovery of the newer bbq devices * fix the test to test the right thing * verify no outstanding flows * only accept entities that match the platform * refactor * refactor * refactor * Refactor PassiveBluetoothDataUpdateCoordinator to support multiple platforms * cover * Update for new model * Update for new model * Update tests/components/govee_ble/test_sensor.py Co-authored-by: Martin Hjelmare * purge dead code * backmerge from integration * Update docstring * Update docstring Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 + .../components/govee_ble/__init__.py | 40 +++++ .../components/govee_ble/config_flow.py | 93 ++++++++++ homeassistant/components/govee_ble/const.py | 3 + .../components/govee_ble/manifest.json | 27 +++ homeassistant/components/govee_ble/sensor.py | 157 ++++++++++++++++ .../components/govee_ble/strings.json | 21 +++ .../components/govee_ble/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 27 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/govee_ble/__init__.py | 38 ++++ tests/components/govee_ble/conftest.py | 8 + .../components/govee_ble/test_config_flow.py | 170 ++++++++++++++++++ tests/components/govee_ble/test_sensor.py | 50 ++++++ 16 files changed, 664 insertions(+) create mode 100644 homeassistant/components/govee_ble/__init__.py create mode 100644 homeassistant/components/govee_ble/config_flow.py create mode 100644 homeassistant/components/govee_ble/const.py create mode 100644 homeassistant/components/govee_ble/manifest.json create mode 100644 homeassistant/components/govee_ble/sensor.py create mode 100644 homeassistant/components/govee_ble/strings.json create mode 100644 homeassistant/components/govee_ble/translations/en.json create mode 100644 tests/components/govee_ble/__init__.py create mode 100644 tests/components/govee_ble/conftest.py create mode 100644 tests/components/govee_ble/test_config_flow.py create mode 100644 tests/components/govee_ble/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c5bfb0cf24c..66e54b25b9e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -411,6 +411,8 @@ build.json @home-assistant/supervisor /homeassistant/components/google_cloud/ @lufton /homeassistant/components/google_travel_time/ @eifinger /tests/components/google_travel_time/ @eifinger +/homeassistant/components/govee_ble/ @bdraco +/tests/components/govee_ble/ @bdraco /homeassistant/components/gpsd/ @fabaff /homeassistant/components/gree/ @cmroche /tests/components/gree/ @cmroche diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py new file mode 100644 index 00000000000..3cb9e4659d6 --- /dev/null +++ b/homeassistant/components/govee_ble/__init__.py @@ -0,0 +1,40 @@ +"""The Govee Bluetooth BLE integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Govee BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py new file mode 100644 index 00000000000..9f2efac0ce9 --- /dev/null +++ b/homeassistant/components/govee_ble/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for govee ble integration.""" +from __future__ import annotations + +from typing import Any + +from govee_ble import GoveeBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for govee.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/govee_ble/const.py b/homeassistant/components/govee_ble/const.py new file mode 100644 index 00000000000..4f30ee5023f --- /dev/null +++ b/homeassistant/components/govee_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the Govee Bluetooth integration.""" + +DOMAIN = "govee_ble" diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json new file mode 100644 index 00000000000..aa86215da59 --- /dev/null +++ b/homeassistant/components/govee_ble/manifest.json @@ -0,0 +1,27 @@ +{ + "domain": "govee_ble", + "name": "Govee Bluetooth", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/govee_ble", + "bluetooth": [ + { "local_name": "Govee*" }, + { "local_name": "GVH5*" }, + { "local_name": "B5178*" }, + { + "manufacturer_id": 26589, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 18994, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 14474, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + } + ], + "requirements": ["govee-ble==0.12.3"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py new file mode 100644 index 00000000000..b8af7df8fb9 --- /dev/null +++ b/homeassistant/components/govee_ble/sensor.py @@ -0,0 +1,157 @@ +"""Support for govee ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from govee_ble import ( + DeviceClass, + DeviceKey, + GoveeBluetoothDeviceData, + SensorDeviceInfo, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to hass device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Govee BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + data = GoveeBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + processor.async_add_entities_listener( + GoveeBluetoothSensorEntity, async_add_entities + ) + ) + + +class GoveeBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a govee ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/govee_ble/strings.json b/homeassistant/components/govee_ble/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/govee_ble/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/govee_ble/translations/en.json b/homeassistant/components/govee_ble/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 4e527a02bc0..f9dd4352e28 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,33 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, str | int | list[int]]] = [ + { + "domain": "govee_ble", + "local_name": "Govee*" + }, + { + "domain": "govee_ble", + "local_name": "GVH5*" + }, + { + "domain": "govee_ble", + "local_name": "B5178*" + }, + { + "domain": "govee_ble", + "manufacturer_id": 26589, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "govee_ble", + "manufacturer_id": 18994, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "govee_ble", + "manufacturer_id": 14474, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, { "domain": "homekit_controller", "manufacturer_id": 76, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 33bd4ea0a52..28d0ad6b44b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -138,6 +138,7 @@ FLOWS = { "goodwe", "google", "google_travel_time", + "govee_ble", "gpslogger", "gree", "growatt_server", diff --git a/requirements_all.txt b/requirements_all.txt index e0f3abff185..512c4535b47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,6 +760,9 @@ googlemaps==2.5.1 # homeassistant.components.slide goslide-api==0.5.1 +# homeassistant.components.govee_ble +govee-ble==0.12.3 + # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee41c383f4a..a805e61fba2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -560,6 +560,9 @@ google-nest-sdm==2.0.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 +# homeassistant.components.govee_ble +govee-ble==0.12.3 + # homeassistant.components.gree greeclimate==1.2.0 diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py new file mode 100644 index 00000000000..3baea5e1140 --- /dev/null +++ b/tests/components/govee_ble/__init__.py @@ -0,0 +1,38 @@ +"""Tests for the Govee BLE integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_GOVEE_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +GVH5075_SERVICE_INFO = BluetoothServiceInfo( + name="GVH5075_2762", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={ + 60552: b"\x00\x03A\xc2d\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\x0c" + }, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) + +GVH5177_SERVICE_INFO = BluetoothServiceInfo( + name="GVH5177_2EC8", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={ + 1: b"\x01\x01\x036&dL\x00\x02\x15INTELLI_ROCKS_HWQw\xf2\xff\xc2" + }, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) diff --git a/tests/components/govee_ble/conftest.py b/tests/components/govee_ble/conftest.py new file mode 100644 index 00000000000..382854a5a28 --- /dev/null +++ b/tests/components/govee_ble/conftest.py @@ -0,0 +1,8 @@ +"""Govee session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py new file mode 100644 index 00000000000..a1b9fed3cd7 --- /dev/null +++ b/tests/components/govee_ble/test_config_flow.py @@ -0,0 +1,170 @@ +"""Test the Govee config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.govee_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import GVH5075_SERVICE_INFO, GVH5177_SERVICE_INFO, NOT_GOVEE_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5075_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "H5075_2762" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + +async def test_async_step_bluetooth_not_govee(hass): + """Test discovery via bluetooth not govee.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_GOVEE_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "H5177_2EC8" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "H5177_2EC8" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py new file mode 100644 index 00000000000..7a6ecbaed51 --- /dev/null +++ b/tests/components/govee_ble/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Govee BLE sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.govee_ble.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import GVH5075_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(GVH5075_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.h5075_2762_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "21.3442" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075_2762 Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 79be87f9ce7e5e624cbd7b7457b99cc06dbf466b Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 24 Jul 2022 10:48:22 +0200 Subject: [PATCH 661/820] Update pyotgw to 2.0.1 (#75663) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index dfb60413721..0bc69387d0b 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.0"], + "requirements": ["pyotgw==2.0.1"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 512c4535b47..3186aee8707 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1732,7 +1732,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.0 +pyotgw==2.0.1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a805e61fba2..b8b42ec87e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1190,7 +1190,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.0 +pyotgw==2.0.1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 198167a2c82a4a7358491a259d91b253b8d8ad0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 11:38:45 -0500 Subject: [PATCH 662/820] Update switchbot to be local push (#75645) * Update switchbot to be local push * fixes * fixes * fixes * fixes * adjust * cover is not assumed anymore * cleanups * adjust * adjust * add missing cover * import compat * fixes * uses lower * uses lower * bleak users upper case addresses * fixes * bump * keep conf_mac and deprecated options for rollback * reuse coordinator * adjust * move around * move around * move around * move around * refactor fixes * compat with DataUpdateCoordinator * fix available * Update homeassistant/components/bluetooth/passive_update_processor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/bluetooth/passive_update_coordinator.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/bluetooth/update_coordinator.py Co-authored-by: Martin Hjelmare * Split bluetooth coordinator into PassiveBluetoothDataUpdateCoordinator and PassiveBluetoothProcessorCoordinator The PassiveBluetoothDataUpdateCoordinator is now used to replace instances of DataUpdateCoordinator where the data is coming from bluetooth advertisements, and the integration may also mix in active updates The PassiveBluetoothProcessorCoordinator is used for integrations that want to process each bluetooth advertisement with multiple processors which can be dispatched to individual platforms or areas or the integration as it chooes * change connections * reduce code churn to reduce review overhead * reduce code churn to reduce review overhead * Update homeassistant/components/bluetooth/passive_update_coordinator.py Co-authored-by: Martin Hjelmare * add basic test * add basic test * complete coverage * Update homeassistant/components/switchbot/coordinator.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/switchbot/coordinator.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/switchbot/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/switchbot/__init__.py Co-authored-by: Martin Hjelmare * lint Co-authored-by: Martin Hjelmare --- .../components/switchbot/__init__.py | 120 +++++---- .../components/switchbot/binary_sensor.py | 27 +- .../components/switchbot/config_flow.py | 143 +++++------ homeassistant/components/switchbot/const.py | 9 +- .../components/switchbot/coordinator.py | 82 ++++--- homeassistant/components/switchbot/cover.py | 43 ++-- homeassistant/components/switchbot/entity.py | 35 ++- .../components/switchbot/manifest.json | 5 +- homeassistant/components/switchbot/sensor.py | 29 +-- .../components/switchbot/strings.json | 8 +- homeassistant/components/switchbot/switch.py | 37 ++- .../components/switchbot/translations/en.json | 9 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/__init__.py | 82 ++++++- tests/components/switchbot/conftest.py | 139 +---------- .../components/switchbot/test_config_flow.py | 232 ++++++++++++++---- 17 files changed, 542 insertions(+), 462 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index d4418685cff..2dbc66a864d 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -1,10 +1,24 @@ """Support for Switchbot devices.""" +from collections.abc import Mapping +import logging +from types import MappingProxyType +from typing import Any + import switchbot +from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_SENSOR_TYPE, Platform +from homeassistant.const import ( + CONF_ADDRESS, + CONF_MAC, + CONF_PASSWORD, + CONF_SENSOR_TYPE, + Platform, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from .const import ( ATTR_BOT, @@ -13,13 +27,8 @@ from .const import ( COMMON_OPTIONS, CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT, - CONF_TIME_BETWEEN_UPDATE_COMMAND, - DATA_COORDINATOR, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, - DEFAULT_SCAN_TIMEOUT, - DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, DOMAIN, ) from .coordinator import SwitchbotDataUpdateCoordinator @@ -29,57 +38,67 @@ PLATFORMS_BY_TYPE = { ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], } +CLASS_BY_DEVICE = { + ATTR_CURTAIN: switchbot.SwitchbotCurtain, + ATTR_BOT: switchbot.Switchbot, +} + +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Switchbot from a config entry.""" hass.data.setdefault(DOMAIN, {}) + domain_data = hass.data[DOMAIN] - if not entry.options: - options = { - CONF_TIME_BETWEEN_UPDATE_COMMAND: DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, - CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT, - CONF_RETRY_TIMEOUT: DEFAULT_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT: DEFAULT_SCAN_TIMEOUT, - } - - hass.config_entries.async_update_entry(entry, options=options) - - # Use same coordinator instance for all entities. - # Uses BTLE advertisement data, all Switchbot devices in range is stored here. - if DATA_COORDINATOR not in hass.data[DOMAIN]: - - if COMMON_OPTIONS not in hass.data[DOMAIN]: - hass.data[DOMAIN][COMMON_OPTIONS] = {**entry.options} - - switchbot.DEFAULT_RETRY_TIMEOUT = hass.data[DOMAIN][COMMON_OPTIONS][ - CONF_RETRY_TIMEOUT - ] - - # Store api in coordinator. - coordinator = SwitchbotDataUpdateCoordinator( - hass, - update_interval=hass.data[DOMAIN][COMMON_OPTIONS][ - CONF_TIME_BETWEEN_UPDATE_COMMAND - ], - api=switchbot, - retry_count=hass.data[DOMAIN][COMMON_OPTIONS][CONF_RETRY_COUNT], - scan_timeout=hass.data[DOMAIN][COMMON_OPTIONS][CONF_SCAN_TIMEOUT], + if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data: + # Bleak uses addresses not mac addresses which are are actually + # UUIDs on some platforms (MacOS). + mac = entry.data[CONF_MAC] + if "-" not in mac: + mac = dr.format_mac(mac) + hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_ADDRESS: mac}, ) - hass.data[DOMAIN][DATA_COORDINATOR] = coordinator + if not entry.options: + hass.config_entries.async_update_entry( + entry, + options={ + CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT, + CONF_RETRY_TIMEOUT: DEFAULT_RETRY_TIMEOUT, + }, + ) - else: - coordinator = hass.data[DOMAIN][DATA_COORDINATOR] + sensor_type: str = entry.data[CONF_SENSOR_TYPE] + address: str = entry.data[CONF_ADDRESS] + ble_device = bluetooth.async_ble_device_from_address(hass, address.upper()) + if not ble_device: + raise ConfigEntryNotReady( + f"Could not find Switchbot {sensor_type} with address {address}" + ) - await coordinator.async_config_entry_first_refresh() + if COMMON_OPTIONS not in domain_data: + domain_data[COMMON_OPTIONS] = entry.options + + common_options: Mapping[str, int] = domain_data[COMMON_OPTIONS] + switchbot.DEFAULT_RETRY_TIMEOUT = common_options[CONF_RETRY_TIMEOUT] + + cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) + device = cls( + device=ble_device, + password=entry.data.get(CONF_PASSWORD), + retry_count=entry.options[CONF_RETRY_COUNT], + ) + coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator( + hass, _LOGGER, ble_device, device, common_options + ) + entry.async_on_unload(coordinator.async_start()) + if not await coordinator.async_wait_ready(): + raise ConfigEntryNotReady(f"Switchbot {sensor_type} with {address} not ready") entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - - hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator} - - sensor_type = entry.data[CONF_SENSOR_TYPE] - await hass.config_entries.async_forward_entry_setups( entry, PLATFORMS_BY_TYPE[sensor_type] ) @@ -96,8 +115,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - - if len(hass.config_entries.async_entries(DOMAIN)) == 0: + if not hass.config_entries.async_entries(DOMAIN): hass.data.pop(DOMAIN) return unload_ok @@ -106,8 +124,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" # Update entity options stored in hass. - if {**entry.options} != hass.data[DOMAIN][COMMON_OPTIONS]: - hass.data[DOMAIN][COMMON_OPTIONS] = {**entry.options} - hass.data[DOMAIN].pop(DATA_COORDINATOR) - - await hass.config_entries.async_reload(entry.entry_id) + common_options: MappingProxyType[str, Any] = hass.data[DOMAIN][COMMON_OPTIONS] + if entry.options != common_options: + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index e2a5a951d1d..d644f603697 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -6,13 +6,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -30,23 +29,19 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Switchbot curtain based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotBinarySensor( coordinator, - entry.unique_id, + unique_id, binary_sensor, - entry.data[CONF_MAC], + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], ) - for binary_sensor in coordinator.data[entry.unique_id]["data"] + for binary_sensor in coordinator.data["data"] if binary_sensor in BINARY_SENSOR_TYPES ] ) @@ -58,15 +53,15 @@ class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, + unique_id: str, binary_sensor: str, mac: str, switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, idx, mac, name=switchbot_name) + super().__init__(coordinator, unique_id, mac, name=switchbot_name) self._sensor = binary_sensor - self._attr_unique_id = f"{idx}-{binary_sensor}" + self._attr_unique_id = f"{unique_id}-{binary_sensor}" self._attr_name = f"{switchbot_name} {binary_sensor.title()}" self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index b35ba052d0a..f6f175819b6 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -2,25 +2,26 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast -from switchbot import GetSwitchbotDevices +from switchbot import SwitchBotAdvertisement, parse_advertisement_data import voluptuous as vol +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from .const import ( CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT, - CONF_TIME_BETWEEN_UPDATE_COMMAND, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, - DEFAULT_SCAN_TIMEOUT, - DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, DOMAIN, SUPPORTED_MODEL_TYPES, ) @@ -28,15 +29,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def _btle_connect() -> dict: - """Scan for BTLE advertisement data.""" - - switchbot_devices = await GetSwitchbotDevices().discover() - - if not switchbot_devices: - raise NotConnectedError("Failed to discover switchbot") - - return switchbot_devices +def format_unique_id(address: str) -> str: + """Format the unique ID for a switchbot.""" + return address.replace(":", "").lower() class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): @@ -44,18 +39,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def _get_switchbots(self) -> dict: - """Try to discover nearby Switchbot devices.""" - # asyncio.lock prevents btle adapter exceptions if there are multiple calls to this method. - # store asyncio.lock in hass data if not present. - if DOMAIN not in self.hass.data: - self.hass.data.setdefault(DOMAIN, {}) - - # Discover switchbots nearby. - _btle_adv_data = await _btle_connect() - - return _btle_adv_data - @staticmethod @callback def async_get_options_flow( @@ -66,62 +49,79 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self._discovered_devices = {} + self._discovered_adv: SwitchBotAdvertisement | None = None + self._discovered_advs: dict[str, SwitchBotAdvertisement] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) + await self.async_set_unique_id(format_unique_id(discovery_info.address)) + self._abort_if_unique_id_configured() + discovery_info_bleak = cast(BluetoothServiceInfoBleak, discovery_info) + parsed = parse_advertisement_data( + discovery_info_bleak.device, discovery_info_bleak.advertisement + ) + if not parsed or parsed.data.get("modelName") not in SUPPORTED_MODEL_TYPES: + return self.async_abort(reason="not_supported") + self._discovered_adv = parsed + data = parsed.data + self.context["title_placeholders"] = { + "name": data["modelName"], + "address": discovery_info.address, + } + return await self.async_step_user() async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle a flow initiated by the user.""" - + """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_MAC].replace(":", "")) + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id( + format_unique_id(address), raise_on_progress=False + ) self._abort_if_unique_id_configured() - user_input[CONF_SENSOR_TYPE] = SUPPORTED_MODEL_TYPES[ - self._discovered_devices[self.unique_id]["modelName"] + self._discovered_advs[address].data["modelName"] ] - return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) - try: - self._discovered_devices = await self._get_switchbots() - for device in self._discovered_devices.values(): - _LOGGER.debug("Found %s", device) + if discovery := self._discovered_adv: + self._discovered_advs[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if ( + format_unique_id(address) in current_addresses + or address in self._discovered_advs + ): + continue + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + if parsed and parsed.data.get("modelName") in SUPPORTED_MODEL_TYPES: + self._discovered_advs[address] = parsed - except NotConnectedError: - return self.async_abort(reason="cannot_connect") - - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - return self.async_abort(reason="unknown") - - # Get devices already configured. - configured_devices = { - item.data[CONF_MAC] - for item in self._async_current_entries(include_ignore=False) - } - - # Get supported devices not yet configured. - unconfigured_devices = { - device["mac_address"]: f"{device['mac_address']} {device['modelName']}" - for device in self._discovered_devices.values() - if device.get("modelName") in SUPPORTED_MODEL_TYPES - and device["mac_address"] not in configured_devices - } - - if not unconfigured_devices: + if not self._discovered_advs: return self.async_abort(reason="no_unconfigured_devices") data_schema = vol.Schema( { - vol.Required(CONF_MAC): vol.In(unconfigured_devices), + vol.Required(CONF_ADDRESS): vol.In( + { + address: f"{parsed.data['modelName']} ({address})" + for address, parsed in self._discovered_advs.items() + } + ), vol.Required(CONF_NAME): str, vol.Optional(CONF_PASSWORD): str, } ) - return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) @@ -148,13 +148,6 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): return self.async_create_entry(title="", data=user_input) options = { - vol.Optional( - CONF_TIME_BETWEEN_UPDATE_COMMAND, - default=self.config_entry.options.get( - CONF_TIME_BETWEEN_UPDATE_COMMAND, - DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, - ), - ): int, vol.Optional( CONF_RETRY_COUNT, default=self.config_entry.options.get( @@ -167,16 +160,6 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): CONF_RETRY_TIMEOUT, DEFAULT_RETRY_TIMEOUT ), ): int, - vol.Optional( - CONF_SCAN_TIMEOUT, - default=self.config_entry.options.get( - CONF_SCAN_TIMEOUT, DEFAULT_SCAN_TIMEOUT - ), - ): int, } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) - - -class NotConnectedError(Exception): - """Exception for unable to find device.""" diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index a363c030eb1..a841b8388dd 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -16,15 +16,14 @@ SUPPORTED_MODEL_TYPES = { # Config Defaults DEFAULT_RETRY_COUNT = 3 DEFAULT_RETRY_TIMEOUT = 5 -DEFAULT_TIME_BETWEEN_UPDATE_COMMAND = 60 -DEFAULT_SCAN_TIMEOUT = 5 # Config Options -CONF_TIME_BETWEEN_UPDATE_COMMAND = "update_time" CONF_RETRY_COUNT = "retry_count" -CONF_RETRY_TIMEOUT = "retry_timeout" CONF_SCAN_TIMEOUT = "scan_timeout" +# Deprecated config Entry Options to be removed in 2023.4 +CONF_TIME_BETWEEN_UPDATE_COMMAND = "update_time" +CONF_RETRY_TIMEOUT = "retry_timeout" + # Data -DATA_COORDINATOR = "coordinator" COMMON_OPTIONS = "common_options" diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 4a4831f2cdb..74cc54402d1 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -1,15 +1,22 @@ """Provides the switchbot DataUpdateCoordinator.""" from __future__ import annotations -from datetime import timedelta +import asyncio +from collections.abc import Mapping import logging +from typing import Any, cast +from bleak.backends.device import BLEDevice import switchbot +from switchbot import parse_advertisement_data -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN +from .const import CONF_RETRY_COUNT _LOGGER = logging.getLogger(__name__) @@ -22,40 +29,53 @@ def flatten_sensors_data(sensor): return sensor -class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): +class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): """Class to manage fetching switchbot data.""" def __init__( self, hass: HomeAssistant, - *, - update_interval: int, - api: switchbot, - retry_count: int, - scan_timeout: int, + logger: logging.Logger, + ble_device: BLEDevice, + device: switchbot.SwitchbotDevice, + common_options: Mapping[str, int], ) -> None: """Initialize global switchbot data updater.""" - self.switchbot_api = api - self.switchbot_data = self.switchbot_api.GetSwitchbotDevices() - self.retry_count = retry_count - self.scan_timeout = scan_timeout - self.update_interval = timedelta(seconds=update_interval) + super().__init__(hass, logger, ble_device.address) + self.ble_device = ble_device + self.device = device + self.common_options = common_options + self.data: dict[str, Any] = {} + self._ready_event = asyncio.Event() - super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=self.update_interval - ) + @property + def retry_count(self) -> int: + """Return retry count.""" + return self.common_options[CONF_RETRY_COUNT] - async def _async_update_data(self) -> dict | None: - """Fetch data from switchbot.""" + @callback + def _async_handle_bluetooth_event( + self, + service_info: bluetooth.BluetoothServiceInfo, + change: bluetooth.BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + super()._async_handle_bluetooth_event(service_info, change) + discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) + if adv := parse_advertisement_data( + discovery_info_bleak.device, discovery_info_bleak.advertisement + ): + self.data = flatten_sensors_data(adv.data) + if "modelName" in self.data: + self._ready_event.set() + _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) + self.device.update_from_advertisement(adv) + self.async_update_listeners() - switchbot_data = await self.switchbot_data.discover( - retry=self.retry_count, scan_timeout=self.scan_timeout - ) - - if not switchbot_data: - raise UpdateFailed("Unable to fetch switchbot services data") - - return { - identifier: flatten_sensors_data(sensor) - for identifier, sensor in switchbot_data.items() - } + async def async_wait_ready(self) -> bool: + """Wait for the device to be ready.""" + try: + await asyncio.wait_for(self._ready_event.wait(), timeout=55) + except asyncio.TimeoutError: + return False + return True diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 9223217c173..dc9ddf4e616 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -14,13 +14,12 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_RETRY_COUNT, DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -33,25 +32,17 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Switchbot curtain based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotCurtainEntity( coordinator, - entry.unique_id, - entry.data[CONF_MAC], + unique_id, + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], - coordinator.switchbot_api.SwitchbotCurtain( - mac=entry.data[CONF_MAC], - password=entry.data.get(CONF_PASSWORD), - retry_count=entry.options[CONF_RETRY_COUNT], - ), + coordinator.device, ) ] ) @@ -67,19 +58,18 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): | CoverEntityFeature.STOP | CoverEntityFeature.SET_POSITION ) - _attr_assumed_state = True def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, - mac: str, + unique_id: str, + address: str, name: str, device: SwitchbotCurtain, ) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, idx, mac, name) - self._attr_unique_id = idx + super().__init__(coordinator, unique_id, address, name) + self._attr_unique_id = unique_id self._attr_is_closed = None self._device = device @@ -97,21 +87,21 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the curtain.""" - _LOGGER.debug("Switchbot to open curtain %s", self._mac) + _LOGGER.debug("Switchbot to open curtain %s", self._address) self._last_run_success = bool(await self._device.open()) self.async_write_ha_state() async def async_close_cover(self, **kwargs: Any) -> None: """Close the curtain.""" - _LOGGER.debug("Switchbot to close the curtain %s", self._mac) + _LOGGER.debug("Switchbot to close the curtain %s", self._address) self._last_run_success = bool(await self._device.close()) self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the moving of this device.""" - _LOGGER.debug("Switchbot to stop %s", self._mac) + _LOGGER.debug("Switchbot to stop %s", self._address) self._last_run_success = bool(await self._device.stop()) self.async_write_ha_state() @@ -119,7 +109,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Move the cover shutter to a specific position.""" position = kwargs.get(ATTR_POSITION) - _LOGGER.debug("Switchbot to move at %d %s", position, self._mac) + _LOGGER.debug("Switchbot to move at %d %s", position, self._address) self._last_run_success = bool(await self._device.set_position(position)) self.async_write_ha_state() @@ -128,4 +118,5 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Handle updated data from the coordinator.""" self._attr_current_cover_position = self.data["data"]["position"] self._attr_is_closed = self.data["data"]["position"] <= 20 + self._attr_is_opening = self.data["data"]["inMotion"] self.async_write_ha_state() diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index c27c40613c7..4e69da4ec11 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -4,43 +4,58 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, +) +from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.entity import DeviceInfo from .const import MANUFACTURER from .coordinator import SwitchbotDataUpdateCoordinator -class SwitchbotEntity(CoordinatorEntity[SwitchbotDataUpdateCoordinator], Entity): +class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): """Generic entity encapsulating common features of Switchbot device.""" + coordinator: SwitchbotDataUpdateCoordinator + def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, - mac: str, + unique_id: str, + address: str, name: str, ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._last_run_success: bool | None = None - self._idx = idx - self._mac = mac + self._unique_id = unique_id + self._address = address self._attr_name = name self._attr_device_info = DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self._mac)}, + connections={(dr.CONNECTION_BLUETOOTH, self._address)}, manufacturer=MANUFACTURER, model=self.data["modelName"], name=name, ) + if ":" not in self._address: + # MacOS Bluetooth addresses are not mac addresses + return + # If the bluetooth address is also a mac address, + # add this connection as well to prevent a new device + # entry from being created when upgrading from a previous + # version of the integration. + self._attr_device_info[ATTR_CONNECTIONS].add( + (dr.CONNECTION_NETWORK_MAC, self._address) + ) @property def data(self) -> dict[str, Any]: """Return coordinator data for this entity.""" - return self.coordinator.data[self._idx] + return self.coordinator.data @property def extra_state_attributes(self) -> Mapping[Any, Any]: """Return the state attributes.""" - return {"last_run_success": self._last_run_success, "mac_address": self._mac} + return {"last_run_success": self._last_run_success} diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 76b43628ab3..c23891083a4 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,10 +2,11 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.14.1"], + "requirements": ["PySwitchbot==0.15.0"], "config_flow": true, + "dependencies": ["bluetooth"], "codeowners": ["@danielhiversen", "@RenierM26", "@murtas"], "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], - "iot_class": "local_polling", + "iot_class": "local_push", "loggers": ["switchbot"] } diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 0bc7fe8a3b2..25863a57df5 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -8,18 +8,17 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_MAC, + CONF_ADDRESS, CONF_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -61,23 +60,19 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Switchbot sensor based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotSensor( coordinator, - entry.unique_id, + unique_id, sensor, - entry.data[CONF_MAC], + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], ) - for sensor in coordinator.data[entry.unique_id]["data"] + for sensor in coordinator.data["data"] if sensor in SENSOR_TYPES ] ) @@ -89,15 +84,15 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, + unique_id: str, sensor: str, - mac: str, + address: str, switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, idx, mac, name=switchbot_name) + super().__init__(coordinator, unique_id, address, name=switchbot_name) self._sensor = sensor - self._attr_unique_id = f"{idx}-{sensor}" + self._attr_unique_id = f"{unique_id}-{sensor}" self._attr_name = f"{switchbot_name} {sensor.title()}" self.entity_description = SENSOR_TYPES[sensor] diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 8c308083982..f0758d767aa 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -1,11 +1,11 @@ { "config": { - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "title": "Setup Switchbot device", "data": { - "mac": "Device MAC address", + "address": "Device address", "name": "[%key:common::config_flow::data::name%]", "password": "[%key:common::config_flow::data::password%]" } @@ -24,10 +24,8 @@ "step": { "init": { "data": { - "update_time": "Time between updates (seconds)", "retry_count": "Retry count", - "retry_timeout": "Timeout between retries", - "scan_timeout": "How long to scan for advertisement data" + "retry_timeout": "Timeout between retries" } } } diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 404a92eda82..51f15c488d1 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -8,13 +8,12 @@ from switchbot import Switchbot from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, STATE_ON +from homeassistant.const import CONF_ADDRESS, CONF_NAME, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_RETRY_COUNT, DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -29,25 +28,17 @@ async def async_setup_entry( async_add_entities: entity_platform.AddEntitiesCallback, ) -> None: """Set up Switchbot based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotBotEntity( coordinator, - entry.unique_id, - entry.data[CONF_MAC], + unique_id, + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], - coordinator.switchbot_api.Switchbot( - mac=entry.data[CONF_MAC], - password=entry.data.get(CONF_PASSWORD), - retry_count=entry.options[CONF_RETRY_COUNT], - ), + coordinator.device, ) ] ) @@ -61,14 +52,14 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, - mac: str, + unique_id: str, + address: str, name: str, device: Switchbot, ) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, idx, mac, name) - self._attr_unique_id = idx + super().__init__(coordinator, unique_id, address, name) + self._attr_unique_id = unique_id self._device = device self._attr_is_on = False @@ -82,7 +73,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" - _LOGGER.info("Turn Switchbot bot on %s", self._mac) + _LOGGER.info("Turn Switchbot bot on %s", self._address) self._last_run_success = bool(await self._device.turn_on()) if self._last_run_success: @@ -91,7 +82,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" - _LOGGER.info("Turn Switchbot bot off %s", self._mac) + _LOGGER.info("Turn Switchbot bot off %s", self._address) self._last_run_success = bool(await self._device.turn_off()) if self._last_run_success: diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 1cfaee8750f..15127b82101 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,11 +7,12 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, - "flow_title": "{name}", + "error": {}, + "flow_title": "{name} ({address})", "step": { "user": { "data": { - "mac": "Device MAC address", + "address": "Device address", "name": "Name", "password": "Password" }, @@ -24,9 +25,7 @@ "init": { "data": { "retry_count": "Retry count", - "retry_timeout": "Timeout between retries", - "scan_timeout": "How long to scan for advertisement data", - "update_time": "Time between updates (seconds)" + "retry_timeout": "Timeout between retries" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 3186aee8707..e9fc510330e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.1 +PySwitchbot==0.15.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8b42ec87e8..235d69e091c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.1 +PySwitchbot==0.15.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 8f1501bfa9a..b4b2e56b39c 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -1,7 +1,11 @@ """Tests for the switchbot integration.""" from unittest.mock import patch -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -11,37 +15,37 @@ DOMAIN = "switchbot" ENTRY_CONFIG = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:99:99:99", + CONF_ADDRESS: "e7:89:43:99:99:99", } USER_INPUT = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:99:99:99", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_CURTAIN = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:90:90:90", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_SENSOR = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "c0:ce:b0:d4:26:be", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_UNSUPPORTED_DEVICE = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "test", + CONF_ADDRESS: "test", } USER_INPUT_INVALID = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "invalid-mac", + CONF_ADDRESS: "invalid-mac", } @@ -68,3 +72,67 @@ async def init_integration( await hass.async_block_till_done() return entry + + +WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), +) +WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoCurtain", + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={89: b"\xc1\xc7'}U\xab"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Y\x00\x11\x04"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoCurtain", + manufacturer_data={89: b"\xc1\xc7'}U\xab"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Y\x00\x11\x04"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoCurtain"), +) + +WOSENSORTH_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoSensorTH", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={2409: b"\xda,\x1e\xb1\x86Au\x03\x00\x96\xac"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00d\x00\x96\xac"}, + rssi=-60, + source="local", + advertisement=AdvertisementData( + manufacturer_data={2409: b"\xda,\x1e\xb1\x86Au\x03\x00\x96\xac"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00d\x00\x96\xac"}, + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoSensorTH"), +) + +NOT_SWITCHBOT_INFO = BluetoothServiceInfoBleak( + name="unknown", + service_uuids=[], + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={}, + service_data={}, + rssi=-60, + source="local", + advertisement=AdvertisementData( + manufacturer_data={}, + service_data={}, + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "unknown"), +) diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 2e6421f22a4..3df082c4361 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -1,139 +1,8 @@ """Define fixtures available for all tests.""" -import sys -from unittest.mock import MagicMock, patch -from pytest import fixture +import pytest -class MocGetSwitchbotDevices: - """Scan for all Switchbot devices and return by type.""" - - def __init__(self, interface=None) -> None: - """Get switchbot devices class constructor.""" - self._interface = interface - self._all_services_data = { - "e78943999999": { - "mac_address": "e7:89:43:99:99:99", - "isEncrypted": False, - "model": "H", - "data": { - "switchMode": "true", - "isOn": "true", - "battery": 91, - "rssi": -71, - }, - "modelName": "WoHand", - }, - "e78943909090": { - "mac_address": "e7:89:43:90:90:90", - "isEncrypted": False, - "model": "c", - "data": { - "calibration": True, - "battery": 74, - "inMotion": False, - "position": 100, - "lightLevel": 2, - "deviceChain": 1, - "rssi": -73, - }, - "modelName": "WoCurtain", - }, - "ffffff19ffff": { - "mac_address": "ff:ff:ff:19:ff:ff", - "isEncrypted": False, - "model": "m", - "rawAdvData": "000d6d00", - }, - "c0ceb0d426be": { - "mac_address": "c0:ce:b0:d4:26:be", - "isEncrypted": False, - "data": { - "temp": {"c": 21.6, "f": 70.88}, - "fahrenheit": False, - "humidity": 73, - "battery": 100, - "rssi": -58, - }, - "model": "T", - "modelName": "WoSensorTH", - }, - } - self._curtain_all_services_data = { - "mac_address": "e7:89:43:90:90:90", - "isEncrypted": False, - "model": "c", - "data": { - "calibration": True, - "battery": 74, - "position": 100, - "lightLevel": 2, - "rssi": -73, - }, - "modelName": "WoCurtain", - } - self._sensor_data = { - "mac_address": "c0:ce:b0:d4:26:be", - "isEncrypted": False, - "data": { - "temp": {"c": 21.6, "f": 70.88}, - "fahrenheit": False, - "humidity": 73, - "battery": 100, - "rssi": -58, - }, - "model": "T", - "modelName": "WoSensorTH", - } - self._unsupported_device = { - "mac_address": "test", - "isEncrypted": False, - "model": "HoN", - "data": { - "switchMode": "true", - "isOn": "true", - "battery": 91, - "rssi": -71, - }, - "modelName": "WoOther", - } - - async def discover(self, retry=0, scan_timeout=0): - """Mock discover.""" - return self._all_services_data - - async def get_device_data(self, mac=None): - """Return data for specific device.""" - if mac == "e7:89:43:99:99:99": - return self._all_services_data - if mac == "test": - return self._unsupported_device - if mac == "e7:89:43:90:90:90": - return self._curtain_all_services_data - if mac == "c0:ce:b0:d4:26:be": - return self._sensor_data - - return None - - -class MocNotConnectedError(Exception): - """Mock exception.""" - - -module = type(sys)("switchbot") -module.GetSwitchbotDevices = MocGetSwitchbotDevices -module.NotConnectedError = MocNotConnectedError -sys.modules["switchbot"] = module - - -@fixture -def switchbot_config_flow(hass): - """Mock the bluepy api for easier config flow testing.""" - with patch.object(MocGetSwitchbotDevices, "discover", return_value=True), patch( - "homeassistant.components.switchbot.config_flow.GetSwitchbotDevices" - ) as mock_switchbot: - instance = mock_switchbot.return_value - - instance.discover = MagicMock(return_value=True) - - yield mock_switchbot +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index aa1adb3a16e..b9d1d556b09 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -1,33 +1,104 @@ """Test the switchbot config flow.""" -from homeassistant.components.switchbot.config_flow import NotConnectedError +from unittest.mock import patch + from homeassistant.components.switchbot.const import ( CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT, - CONF_TIME_BETWEEN_UPDATE_COMMAND, ) -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.data_entry_flow import FlowResultType from . import ( + NOT_SWITCHBOT_INFO, USER_INPUT, USER_INPUT_CURTAIN, USER_INPUT_SENSOR, + WOCURTAIN_SERVICE_INFO, + WOHAND_SERVICE_INFO, + WOSENSORTH_SERVICE_INFO, init_integration, patch_async_setup_entry, ) +from tests.common import MockConfigEntry + DOMAIN = "switchbot" -async def test_user_form_valid_mac(hass): +async def test_bluetooth_discovery(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOHAND_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-name" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_discovery_already_setup(hass): + """Test discovery via bluetooth with a valid device when already setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + unique_id="aabbccddeeff", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOHAND_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_not_switchbot(hass): + """Test discovery via bluetooth not switchbot.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=NOT_SWITCHBOT_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_user_setup_wohand(hass): """Test the user initiated form with password and valid mac.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOHAND_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -42,7 +113,7 @@ async def test_user_form_valid_mac(hass): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { - CONF_MAC: "e7:89:43:99:99:99", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", @@ -50,11 +121,41 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 - # test curtain device creation. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} +async def test_user_setup_wohand_already_configured(hass): + """Test the user initiated form with password and valid mac.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + unique_id="aabbccddeeff", ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOHAND_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_setup_wocurtain(hass): + """Test the user initiated form with password and valid mac.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -69,7 +170,7 @@ async def test_user_form_valid_mac(hass): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { - CONF_MAC: "e7:89:43:90:90:90", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "curtain", @@ -77,11 +178,16 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 - # test sensor device creation. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) +async def test_user_setup_wosensor(hass): + """Test the user initiated form with password and valid mac.""" + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOSENSORTH_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -96,7 +202,7 @@ async def test_user_form_valid_mac(hass): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { - CONF_MAC: "c0:ce:b0:d4:26:be", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "hygrometer", @@ -104,39 +210,78 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 - # tests abort if no unconfigured devices are found. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) +async def test_user_no_devices(hass): + """Test the user initiated form with password and valid mac.""" + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_unconfigured_devices" -async def test_user_form_exception(hass, switchbot_config_flow): - """Test we handle exception on user form.""" - - switchbot_config_flow.side_effect = NotConnectedError - +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOCURTAIN_SERVICE_INFO, ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "cannot_connect" + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM - switchbot_config_flow.side_effect = Exception + with patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT, + ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "test-name" + assert result2["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "curtain", + } - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "unknown" + assert len(mock_setup_entry.mock_calls) == 1 + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) async def test_options_flow(hass): """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + options={ + CONF_RETRY_COUNT: 10, + CONF_RETRY_TIMEOUT: 10, + }, + unique_id="aabbccddeeff", + ) + entry.add_to_hass(hass) + with patch_async_setup_entry() as mock_setup_entry: entry = await init_integration(hass) @@ -148,21 +293,17 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_TIME_BETWEEN_UPDATE_COMMAND: 60, CONF_RETRY_COUNT: 3, CONF_RETRY_TIMEOUT: 5, - CONF_SCAN_TIMEOUT: 5, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 60 assert result["data"][CONF_RETRY_COUNT] == 3 assert result["data"][CONF_RETRY_TIMEOUT] == 5 - assert result["data"][CONF_SCAN_TIMEOUT] == 5 - assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 # Test changing of entry options. @@ -177,18 +318,17 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_TIME_BETWEEN_UPDATE_COMMAND: 66, CONF_RETRY_COUNT: 6, CONF_RETRY_TIMEOUT: 6, - CONF_SCAN_TIMEOUT: 6, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 66 assert result["data"][CONF_RETRY_COUNT] == 6 assert result["data"][CONF_RETRY_TIMEOUT] == 6 - assert result["data"][CONF_SCAN_TIMEOUT] == 6 assert len(mock_setup_entry.mock_calls) == 1 + + assert entry.options[CONF_RETRY_COUNT] == 6 + assert entry.options[CONF_RETRY_TIMEOUT] == 6 From 0df08b6b0ce6e8d853ba4981e8f1e1492227a198 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 13:12:16 -0500 Subject: [PATCH 663/820] Bump aiohomekit to 1.2.0 (#75686) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 47472ba66bf..51d753f77fc 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.10"], + "requirements": ["aiohomekit==1.2.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index e9fc510330e..011b57860ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.10 +aiohomekit==1.2.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 235d69e091c..e2dae175598 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.10 +aiohomekit==1.2.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 9fae638f6587087ea9ff708cc0d79d131b079302 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sun, 24 Jul 2022 14:40:42 -0400 Subject: [PATCH 664/820] Migrate ElkM1 to new entity naming style (#75023) --- homeassistant/components/elkm1/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 472d31ccb93..2ce0e726fc4 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -443,13 +443,14 @@ def create_elk_entities( class ElkEntity(Entity): """Base class for all Elk entities.""" + _attr_has_entity_name = True + def __init__(self, element: Element, elk: Elk, elk_data: dict[str, Any]) -> None: """Initialize the base of all Elk devices.""" self._elk = elk self._element = element self._mac = elk_data["mac"] self._prefix = elk_data["prefix"] - self._name_prefix = f"{self._prefix} " if self._prefix else "" self._temperature_unit: str = elk_data["config"]["temperature_unit"] # unique_id starts with elkm1_ iff there is no prefix # it starts with elkm1m_{prefix} iff there is a prefix @@ -464,11 +465,7 @@ class ElkEntity(Entity): else: uid_start = "elkm1" self._unique_id = f"{uid_start}_{self._element.default_name('_')}".lower() - - @property - def name(self) -> str: - """Name of the element.""" - return f"{self._name_prefix}{self._element.name}" + self._attr_name = element.name @property def unique_id(self) -> str: From f94a79b409917f5260317b7d498c0c2bcd0d3dcf Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 24 Jul 2022 20:59:25 +0200 Subject: [PATCH 665/820] Bump motionblinds to 0.6.11 (#75581) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index bc09d3e9e38..3499932c1d8 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.8"], + "requirements": ["motionblinds==0.6.11"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index 011b57860ee..d6a4201cfd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.8 +motionblinds==0.6.11 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2dae175598..ae40622f221 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -748,7 +748,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.8 +motionblinds==0.6.11 # homeassistant.components.motioneye motioneye-client==0.3.12 From e18819c678a79b3fc97ae18b6b57d1f5772b1f0f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 24 Jul 2022 20:00:56 +0100 Subject: [PATCH 666/820] Support for encrypted BLE MiBeacon devices (#75677) * Support for encrypted devices * Sensor should use bindkey if available * Error message if encryption fails * Let mypy know this is always set by now * Towards supporting encryption in step_user * Add tests for the 4 new happy paths * Add test coverage for failure cases * Add strings * Bump to 0.5.1. Legacy MiBeacon does not use an authentication token, so harder to detect incorrect key * Add _title() helper * Fix test after rebase * Update homeassistant/components/xiaomi_ble/strings.json Co-authored-by: Martin Hjelmare * Remove unused lines Co-authored-by: Martin Hjelmare --- .../components/xiaomi_ble/config_flow.py | 141 +++++- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 5 +- .../components/xiaomi_ble/strings.json | 17 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/__init__.py | 24 + .../components/xiaomi_ble/test_config_flow.py | 434 ++++++++++++++++++ tests/components/xiaomi_ble/test_sensor.py | 45 ++ 9 files changed, 649 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 8f478442d6a..8b3ec22def7 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -1,10 +1,12 @@ """Config flow for Xiaomi Bluetooth integration.""" from __future__ import annotations +import dataclasses from typing import Any import voluptuous as vol from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData +from xiaomi_ble.parser import EncryptionScheme from homeassistant.components.bluetooth import ( BluetoothServiceInfo, @@ -17,6 +19,19 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN +@dataclasses.dataclass +class Discovery: + """A discovered bluetooth device.""" + + title: str + discovery_info: BluetoothServiceInfo + device: DeviceData + + +def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: + return device.title or device.get_device_name() or discovery_info.name + + class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Xiaomi Bluetooth.""" @@ -26,7 +41,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._discovery_info: BluetoothServiceInfo | None = None self._discovered_device: DeviceData | None = None - self._discovered_devices: dict[str, str] = {} + self._discovered_devices: dict[str, Discovery] = {} async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo @@ -39,25 +54,101 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_supported") self._discovery_info = discovery_info self._discovered_device = device + + title = _title(discovery_info, device) + self.context["title_placeholders"] = {"name": title} + + if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + return await self.async_step_get_encryption_key_legacy() + if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + return await self.async_step_get_encryption_key_4_5() return await self.async_step_bluetooth_confirm() + async def async_step_get_encryption_key_legacy( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Enter a legacy bindkey for a v2/v3 MiBeacon device.""" + assert self._discovery_info + errors = {} + + if user_input is not None: + bindkey = user_input["bindkey"] + + if len(bindkey) != 24: + errors["bindkey"] = "expected_24_characters" + else: + device = DeviceData(bindkey=bytes.fromhex(bindkey)) + + # If we got this far we already know supported will + # return true so we don't bother checking that again + # We just want to retry the decryption + device.supported(self._discovery_info) + + if device.bindkey_verified: + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data={"bindkey": bindkey}, + ) + + errors["bindkey"] = "decryption_failed" + + return self.async_show_form( + step_id="get_encryption_key_legacy", + description_placeholders=self.context["title_placeholders"], + data_schema=vol.Schema({vol.Required("bindkey"): vol.All(str, vol.Strip)}), + errors=errors, + ) + + async def async_step_get_encryption_key_4_5( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Enter a bindkey for a v4/v5 MiBeacon device.""" + assert self._discovery_info + + errors = {} + + if user_input is not None: + bindkey = user_input["bindkey"] + + if len(bindkey) != 32: + errors["bindkey"] = "expected_32_characters" + else: + device = DeviceData(bindkey=bytes.fromhex(bindkey)) + + # If we got this far we already know supported will + # return true so we don't bother checking that again + # We just want to retry the decryption + device.supported(self._discovery_info) + + if device.bindkey_verified: + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data={"bindkey": bindkey}, + ) + + errors["bindkey"] = "decryption_failed" + + return self.async_show_form( + step_id="get_encryption_key_4_5", + description_placeholders=self.context["title_placeholders"], + data_schema=vol.Schema({vol.Required("bindkey"): vol.All(str, vol.Strip)}), + errors=errors, + ) + async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" - assert self._discovered_device is not None - device = self._discovered_device - assert self._discovery_info is not None - discovery_info = self._discovery_info - title = device.title or device.get_device_name() or discovery_info.name if user_input is not None: - return self.async_create_entry(title=title, data={}) + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data={}, + ) self._set_confirm_only() - placeholders = {"name": title} - self.context["title_placeholders"] = placeholders return self.async_show_form( - step_id="bluetooth_confirm", description_placeholders=placeholders + step_id="bluetooth_confirm", + description_placeholders=self.context["title_placeholders"], ) async def async_step_user( @@ -67,9 +158,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) - return self.async_create_entry( - title=self._discovered_devices[address], data={} - ) + discovery = self._discovered_devices[address] + + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + self._discovery_info = discovery.discovery_info + self.context["title_placeholders"] = {"name": discovery.title} + return await self.async_step_get_encryption_key_legacy() + + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + self._discovery_info = discovery.discovery_info + self.context["title_placeholders"] = {"name": discovery.title} + return await self.async_step_get_encryption_key_4_5() + + return self.async_create_entry(title=discovery.title, data={}) current_addresses = self._async_current_ids() for discovery_info in async_discovered_service_info(self.hass): @@ -78,16 +179,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): continue device = DeviceData() if device.supported(discovery_info): - self._discovered_devices[address] = ( - device.title or device.get_device_name() or discovery_info.name + self._discovered_devices[address] = Discovery( + title=_title(discovery_info, device), + discovery_info=discovery_info, + device=device, ) if not self._discovered_devices: return self.async_abort(reason="no_devices_found") + titles = { + address: discovery.title + for (address, discovery) in self._discovered_devices.items() + } return self.async_show_form( step_id="user", - data_schema=vol.Schema( - {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} - ), + data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), ) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 17e22accd6d..457eac60407 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.2.0"], + "requirements": ["xiaomi-ble==0.5.1"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 50cbb0f66cf..9cdf661f5ad 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -158,7 +158,10 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = XiaomiBluetoothDeviceData() + kwargs = {} + if bindkey := entry.data.get("bindkey"): + kwargs["bindkey"] = bytes.fromhex(bindkey) + data = XiaomiBluetoothDeviceData(**kwargs) processor = PassiveBluetoothDataProcessor( lambda service_info: sensor_update_to_bluetooth_data_update( data.update(service_info) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 7111626cca1..e12a15a0671 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -10,12 +10,27 @@ }, "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + }, + "get_encryption_key_legacy": { + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", + "data": { + "bindkey": "Bindkey" + } + }, + "get_encryption_key_4_5": { + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 32 character hexadecimal bindkey.", + "data": { + "bindkey": "Bindkey" + } } }, "abort": { "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." } } } diff --git a/requirements_all.txt b/requirements_all.txt index d6a4201cfd5..b078bf9d4d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2477,7 +2477,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.2.0 +xiaomi-ble==0.5.1 # homeassistant.components.knx xknx==0.21.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae40622f221..269a2323760 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.2.0 +xiaomi-ble==0.5.1 # homeassistant.components.knx xknx==0.21.5 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 80ec2f19989..a6269a02d12 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -37,6 +37,30 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( source="local", ) +JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfo( + name="JTYJGD03MI", + address="54:EF:44:E3:9C:BC", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b'XY\x97\td\xbc\x9c\xe3D\xefT" `\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3' + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + +YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( + name="YLKG07YL", + address="F8:24:41:C5:98:8B", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: """Make a dummy advertisement.""" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index f99cbd21296..fc625bdf7ec 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -7,9 +7,11 @@ from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.data_entry_flow import FlowResultType from . import ( + JTYJGD03MI_SERVICE_INFO, LYWSDCGQ_SERVICE_INFO, MMC_T201_1_SERVICE_INFO, NOT_SENSOR_PUSH_SERVICE_INFO, + YLKG07YL_SERVICE_INFO, ) from tests.common import MockConfigEntry @@ -36,6 +38,187 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" +async def test_async_step_bluetooth_valid_device_legacy_encryption(hass): + """Test discovery via bluetooth with a valid device, with legacy encryption.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YLKG07YL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key(hass): + """Test discovery via bluetooth with a valid device, with legacy encryption and invalid key.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YLKG07YL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_length( + hass, +): + """Test discovery via bluetooth with a valid device, with legacy encryption and wrong key length.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YLKG07YL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "expected_24_characters" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_bluetooth_valid_device_v4_encryption(hass): + """Test discovery via bluetooth with a valid device, with v4 encryption.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=JTYJGD03MI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key(hass): + """Test discovery via bluetooth with a valid device, with v4 encryption and wrong key.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=JTYJGD03MI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length(hass): + """Test discovery via bluetooth with a valid device, with v4 encryption and wrong key length.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=JTYJGD03MI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18fda143a58"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "expected_32_characters" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + async def test_async_step_bluetooth_not_xiaomi(hass): """Test discovery via bluetooth not xiaomi.""" result = await hass.config_entries.flow.async_init( @@ -82,6 +265,257 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "58:2D:34:35:93:21" +async def test_async_step_user_with_found_devices_v4_encryption(hass): + """Test setup from service info cache with devices found, with v4 encryption.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[JTYJGD03MI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:EF:44:E3:9C:BC"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_user_with_found_devices_v4_encryption_wrong_key(hass): + """Test setup from service info cache with devices found, with v4 encryption and wrong key.""" + # Get a list of devices + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[JTYJGD03MI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Pick a device + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:EF:44:E3:9C:BC"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + # Try an incorrect key + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Check can still finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length(hass): + """Test setup from service info cache with devices found, with v4 encryption and wrong key length.""" + # Get a list of devices + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[JTYJGD03MI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Select a single device + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:EF:44:E3:9C:BC"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + # Try an incorrect key + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef1dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "expected_32_characters" + + # Check can still finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_user_with_found_devices_legacy_encryption(hass): + """Test setup from service info cache with devices found, with legacy encryption.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[YLKG07YL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F8:24:41:C5:98:8B"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_legacy" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( + hass, +): + """Test setup from service info cache with devices found, with legacy encryption and wrong key.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[YLKG07YL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F8:24:41:C5:98:8B"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_legacy" + + # Enter an incorrect code + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Check you can finish the flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_length( + hass, +): + """Test setup from service info cache with devices found, with legacy encryption and wrong key length.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[YLKG07YL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F8:24:41:C5:98:8B"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_legacy" + + # Enter an incorrect code + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b85307518487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "expected_24_characters" + + # Check you can finish the flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 74a4fe65131..b95ea37311e 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -130,3 +130,48 @@ async def test_xiaomi_HHCCJCY01(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_xiaomi_CGDK2(hass): + """This device has encrypion so we need to retrieve its bindkey from the configentry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:12:20:89", + data={"bindkey": "a3bfe9853dd85a620debe3620caaa351"}, + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "58:2D:34:12:20:89", + b"XXo\x06\x07\x89 \x124-X_\x17m\xd5O\x02\x00\x00/\xa4S\xfa", + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "22.6" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From c0e68520778e5a059311d0fff39a746cb1a8d648 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 24 Jul 2022 21:11:30 +0200 Subject: [PATCH 667/820] Update pip version range to 22.3 (#75572) --- .github/workflows/ci.yaml | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tox.ini | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 29527383bab..24d37b94518 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -517,7 +517,7 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel + pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.3" setuptools wheel pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver pip install -e . diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ad640ff596b..568ba909ba5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ lru-dict==1.1.8 orjson==3.7.8 paho-mqtt==1.6.1 pillow==9.2.0 -pip>=21.0,<22.2 +pip>=21.0,<22.3 pyserial==3.5 python-slugify==4.0.1 pyudev==0.23.2 diff --git a/pyproject.toml b/pyproject.toml index eb1209234be..48d12964414 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", "orjson==3.7.8", - "pip>=21.0,<22.2", + "pip>=21.0,<22.3", "python-slugify==4.0.1", "pyyaml==6.0", "requests==2.28.1", diff --git a/requirements.txt b/requirements.txt index 4785d344e64..ce77253b752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ lru-dict==1.1.8 PyJWT==2.4.0 cryptography==36.0.2 orjson==3.7.8 -pip>=21.0,<22.2 +pip>=21.0,<22.3 python-slugify==4.0.1 pyyaml==6.0 requests==2.28.1 diff --git a/tox.ini b/tox.ini index b39caacf471..b96ab648fa2 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ isolated_build = True [testenv] basepython = {env:PYTHON3_PATH:python3} # pip version duplicated in homeassistant/package_constraints.txt -pip_version = pip>=21.0,<22.2 +pip_version = pip>=21.0,<22.3 install_command = python -m pip install --use-deprecated legacy-resolver {opts} {packages} commands = {envpython} -X dev -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} From 2d4bd4d7c155f579fc7e87b8906551d4e5b6725e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 24 Jul 2022 14:09:02 -0600 Subject: [PATCH 668/820] Revert SimpliSafe auth flow to the quasi-manual OAuth method from 2021.11.0 (#75641) * Revert "Migrate SimpliSafe to new web-based authentication (#57212)" This reverts commit bf7c99c1f8f33720149b58a0a3b1687189b29179. * Tests 100% * Version bump * Add manifest version for custom component testing * Remove manifest version * Code review * Fix tests --- .../components/simplisafe/__init__.py | 4 +- .../components/simplisafe/config_flow.py | 269 ++++--------- .../components/simplisafe/manifest.json | 2 +- .../components/simplisafe/strings.json | 26 +- .../simplisafe/translations/en.json | 26 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/simplisafe/conftest.py | 18 +- .../components/simplisafe/test_config_flow.py | 373 +++++------------- 9 files changed, 197 insertions(+), 525 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 716163c4957..c28a3740694 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -277,10 +277,8 @@ def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> """Bring a config entry up to current standards.""" if CONF_TOKEN not in entry.data: raise ConfigEntryAuthFailed( - "New SimpliSafe OAuth standard requires re-authentication" + "SimpliSafe OAuth standard requires re-authentication" ) - if CONF_USERNAME not in entry.data: - raise ConfigEntryAuthFailed("Need to re-auth with username/password") entry_updates = {} if not entry.unique_id: diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0b95de2c186..0b92871ccb2 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,48 +1,52 @@ """Config flow to configure the SimpliSafe component.""" from __future__ import annotations -import asyncio from collections.abc import Mapping -from typing import Any +from typing import Any, NamedTuple -import async_timeout from simplipy import API -from simplipy.api import AuthStates -from simplipy.errors import InvalidCredentialsError, SimplipyError, Verify2FAPending +from simplipy.errors import InvalidCredentialsError, SimplipyError +from simplipy.util.auth import ( + get_auth0_code_challenge, + get_auth0_code_verifier, + get_auth_url, +) import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_URL, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER -DEFAULT_EMAIL_2FA_SLEEP = 3 -DEFAULT_EMAIL_2FA_TIMEOUT = 600 - -STEP_REAUTH_SCHEMA = vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - } -) - -STEP_SMS_2FA_SCHEMA = vol.Schema( - { - vol.Required(CONF_CODE): cv.string, - } -) +CONF_AUTH_CODE = "auth_code" STEP_USER_SCHEMA = vol.Schema( { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_AUTH_CODE): cv.string, } ) +class SimpliSafeOAuthValues(NamedTuple): + """Define a named tuple to handle SimpliSafe OAuth strings.""" + + auth_url: str + code_verifier: str + + +@callback +def async_get_simplisafe_oauth_values() -> SimpliSafeOAuthValues: + """Get a SimpliSafe OAuth code verifier and auth URL.""" + code_verifier = get_auth0_code_verifier() + code_challenge = get_auth0_code_challenge(code_verifier) + auth_url = get_auth_url(code_challenge) + return SimpliSafeOAuthValues(auth_url, code_verifier) + + class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a SimpliSafe config flow.""" @@ -50,45 +54,8 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._email_2fa_task: asyncio.Task | None = None - self._password: str | None = None + self._oauth_values: SimpliSafeOAuthValues = async_get_simplisafe_oauth_values() self._reauth: bool = False - self._simplisafe: API | None = None - self._username: str | None = None - - async def _async_authenticate( - self, originating_step_id: str, originating_step_schema: vol.Schema - ) -> FlowResult: - """Attempt to authenticate to the SimpliSafe API.""" - assert self._password - assert self._username - - errors = {} - session = aiohttp_client.async_get_clientsession(self.hass) - - try: - self._simplisafe = await API.async_from_credentials( - self._username, self._password, session=session - ) - except InvalidCredentialsError: - errors = {"base": "invalid_auth"} - except SimplipyError as err: - LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) - errors = {"base": "unknown"} - - if errors: - return self.async_show_form( - step_id=originating_step_id, - data_schema=originating_step_schema, - errors=errors, - description_placeholders={CONF_USERNAME: self._username}, - ) - - assert self._simplisafe - - if self._simplisafe.auth_state == AuthStates.PENDING_2FA_SMS: - return await self.async_step_sms_2fa() - return await self.async_step_email_2fa() @staticmethod @callback @@ -98,146 +65,66 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._reauth = True - - if CONF_USERNAME not in entry_data: - # Old versions of the config flow may not have the username by this point; - # in that case, we reauth them by making them go through the user flow: - return await self.async_step_user() - - self._username = entry_data[CONF_USERNAME] - return await self.async_step_reauth_confirm() - - async def _async_get_email_2fa(self) -> None: - """Define a task to wait for email-based 2FA.""" - assert self._simplisafe - - try: - async with async_timeout.timeout(DEFAULT_EMAIL_2FA_TIMEOUT): - while True: - try: - await self._simplisafe.async_verify_2fa_email() - except Verify2FAPending: - LOGGER.info("Email-based 2FA pending; trying again") - await asyncio.sleep(DEFAULT_EMAIL_2FA_SLEEP) - else: - break - finally: - self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) - ) - - async def async_step_email_2fa( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle email-based two-factor authentication.""" - if not self._email_2fa_task: - self._email_2fa_task = self.hass.async_create_task( - self._async_get_email_2fa() - ) - return self.async_show_progress( - step_id="email_2fa", progress_action="email_2fa" - ) - - try: - await self._email_2fa_task - except asyncio.TimeoutError: - return self.async_show_progress_done(next_step_id="email_2fa_error") - return self.async_show_progress_done(next_step_id="finish") - - async def async_step_email_2fa_error( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle an error during email-based two-factor authentication.""" - return self.async_abort(reason="email_2fa_timed_out") - - async def async_step_finish( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the final step.""" - assert self._simplisafe - assert self._username - - data = { - CONF_USERNAME: self._username, - CONF_TOKEN: self._simplisafe.refresh_token, - } - - user_id = str(self._simplisafe.user_id) - - if self._reauth: - # "Old" config entries utilized the user's email address (username) as the - # unique ID, whereas "new" config entries utilize the SimpliSafe user ID – - # only one can exist at a time, but the presence of either one is a - # candidate for re-auth: - if existing_entries := [ - entry - for entry in self.hass.config_entries.async_entries() - if entry.domain == DOMAIN - and entry.unique_id in (self._username, user_id) - ]: - existing_entry = existing_entries[0] - self.hass.config_entries.async_update_entry( - existing_entry, unique_id=user_id, title=self._username, data=data - ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(existing_entry.entry_id) - ) - return self.async_abort(reason="reauth_successful") - - await self.async_set_unique_id(user_id) - self._abort_if_unique_id_configured() - return self.async_create_entry(title=self._username, data=data) - - async def async_step_reauth_confirm( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle re-auth completion.""" - if not user_input: - return self.async_show_form( - step_id="reauth_confirm", - data_schema=STEP_REAUTH_SCHEMA, - description_placeholders={CONF_USERNAME: self._username}, - ) - - self._password = user_input[CONF_PASSWORD] - return await self._async_authenticate("reauth_confirm", STEP_REAUTH_SCHEMA) - - async def async_step_sms_2fa( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle SMS-based two-factor authentication.""" - if not user_input: - return self.async_show_form( - step_id="sms_2fa", - data_schema=STEP_SMS_2FA_SCHEMA, - ) - - assert self._simplisafe - - try: - await self._simplisafe.async_verify_2fa_sms(user_input[CONF_CODE]) - except InvalidCredentialsError: - return self.async_show_form( - step_id="sms_2fa", - data_schema=STEP_SMS_2FA_SCHEMA, - errors={CONF_CODE: "invalid_auth"}, - ) - - return await self.async_step_finish() + return await self.async_step_user() async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the start of the config flow.""" if user_input is None: - return self.async_show_form(step_id="user", data_schema=STEP_USER_SCHEMA) + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) - self._username = user_input[CONF_USERNAME] - self._password = user_input[CONF_PASSWORD] - return await self._async_authenticate("user", STEP_USER_SCHEMA) + errors = {} + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + simplisafe = await API.async_from_auth( + user_input[CONF_AUTH_CODE], + self._oauth_values.code_verifier, + session=session, + ) + except InvalidCredentialsError: + errors = {"base": "invalid_auth"} + except SimplipyError as err: + LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) + errors = {"base": "unknown"} + + if errors: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors=errors, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) + + simplisafe_user_id = str(simplisafe.user_id) + data = {CONF_USERNAME: simplisafe_user_id, CONF_TOKEN: simplisafe.refresh_token} + + if self._reauth: + existing_entry = await self.async_set_unique_id(simplisafe_user_id) + if not existing_entry: + # If we don't have an entry that matches this user ID, the user logged + # in with different credentials: + return self.async_abort(reason="wrong_account") + + self.hass.config_entries.async_update_entry( + existing_entry, unique_id=simplisafe_user_id, data=data + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + await self.async_set_unique_id(simplisafe_user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=simplisafe_user_id, data=data) class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index b6a139fba80..b08799e4082 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2022.07.0"], + "requirements": ["simplisafe-python==2022.07.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 85e579fd455..16ae7111abf 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -1,38 +1,22 @@ { "config": { "step": { - "reauth_confirm": { - "title": "[%key:common::config_flow::title::reauth%]", - "description": "Please re-enter the password for {username}.", - "data": { - "password": "[%key:common::config_flow::data::password%]" - } - }, - "sms_2fa": { - "description": "Input the two-factor authentication code sent to you via SMS.", - "data": { - "code": "Code" - } - }, "user": { - "description": "Input your username and password.", + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL.", "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "auth_code": "Authorization Code" } } }, "error": { + "identifier_exists": "Account already registered", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "wrong_account": "The user credentials provided do not match this SimpliSafe account." } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 0da6f6442e4..82320df4864 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,36 +2,20 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, "error": { + "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Please re-enter the password for {username}.", - "title": "Reauthenticate Integration" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Input the two-factor authentication code sent to you via SMS." - }, "user": { "data": { - "password": "Password", - "username": "Username" + "auth_code": "Authorization Code" }, - "description": "Input your username and password." + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL." } } }, diff --git a/requirements_all.txt b/requirements_all.txt index b078bf9d4d1..fff6dd9a48f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2188,7 +2188,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.07.0 +simplisafe-python==2022.07.1 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 269a2323760..b485db528ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1466,7 +1466,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.07.0 +simplisafe-python==2022.07.1 # homeassistant.components.slack slackclient==2.5.0 diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index 82bd04a7349..54ab7fbe9d7 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -3,7 +3,6 @@ import json from unittest.mock import AsyncMock, Mock, patch import pytest -from simplipy.api import AuthStates from simplipy.system.v3 import SystemV3 from homeassistant.components.simplisafe.const import DOMAIN @@ -19,20 +18,11 @@ PASSWORD = "password" SYSTEM_ID = "system_123" -@pytest.fixture(name="api_auth_state") -def api_auth_state_fixture(): - """Define a SimpliSafe API auth state.""" - return AuthStates.PENDING_2FA_SMS - - @pytest.fixture(name="api") -def api_fixture(api_auth_state, data_subscription, system_v3, websocket): +def api_fixture(data_subscription, system_v3, websocket): """Define a simplisafe-python API object.""" return Mock( async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}), - async_verify_2fa_email=AsyncMock(), - async_verify_2fa_sms=AsyncMock(), - auth_state=api_auth_state, refresh_token=REFRESH_TOKEN, subscription_data=data_subscription, user_id=USER_ID, @@ -104,12 +94,10 @@ def reauth_config_fixture(): async def setup_simplisafe_fixture(hass, api, config): """Define a fixture to set up SimpliSafe.""" with patch( - "homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_SLEEP", 0 - ), patch( - "homeassistant.components.simplisafe.config_flow.API.async_from_credentials", + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", return_value=api, ), patch( - "homeassistant.components.simplisafe.API.async_from_credentials", + "homeassistant.components.simplisafe.API.async_from_auth", return_value=api, ), patch( "homeassistant.components.simplisafe.API.async_from_refresh_token", diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 9211ec8ea28..6e6f99ad4bb 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -2,317 +2,148 @@ from unittest.mock import patch import pytest -from simplipy.api import AuthStates -from simplipy.errors import InvalidCredentialsError, SimplipyError, Verify2FAPending +from simplipy.errors import InvalidCredentialsError, SimplipyError from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN +from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME -from .common import REFRESH_TOKEN, USER_ID, USERNAME -from tests.common import MockConfigEntry - -CONF_USER_ID = "user_id" - - -async def test_duplicate_error( - hass, config_entry, credentials_config, setup_simplisafe, sms_config -): +async def test_duplicate_error(config_entry, hass, setup_simplisafe): """Test that errors are shown when duplicates are added.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" -async def test_options_flow(hass, config_entry): +async def test_invalid_credentials(hass): + """Test that invalid credentials show the correct error.""" + with patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + side_effect=InvalidCredentialsError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_options_flow(config_entry, hass): """Test config flow options.""" with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_CODE: "4321"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == {CONF_CODE: "4321"} -@pytest.mark.parametrize("unique_id", [USERNAME, USER_ID]) -async def test_step_reauth( - hass, config, config_entry, reauth_config, setup_simplisafe, sms_config, unique_id -): - """Test the re-auth step (testing both username and user ID as unique ID).""" - # Add a second config entry (tied to a random domain, but with the same unique ID - # that could exist in a SimpliSafe entry) to ensure that this reauth process only - # touches the SimpliSafe entry: - entry = MockConfigEntry(domain="random", unique_id=USERNAME, data={"some": "data"}) - entry.add_to_hass(hass) - +async def test_step_reauth(config_entry, hass, setup_simplisafe): + """Test the re-auth step.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config - ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=reauth_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - - assert len(hass.config_entries.async_entries()) == 2 - - # Test that the SimpliSafe config flow is updated: - [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.unique_id == USER_ID - assert config_entry.data == config - - # Test that the non-SimpliSafe config flow remains the same: - [config_entry] = hass.config_entries.async_entries("random") - assert config_entry == entry - - -@pytest.mark.parametrize( - "exc,error_string", - [(InvalidCredentialsError, "invalid_auth"), (SimplipyError, "unknown")], -) -async def test_step_reauth_errors(hass, config, error_string, exc, reauth_config): - """Test that errors during the reauth step are handled.""" - with patch( - "homeassistant.components.simplisafe.API.async_from_credentials", - side_effect=exc, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config - ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=reauth_config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": error_string} - - -@pytest.mark.parametrize( - "config,unique_id", - [ - ( - { - CONF_TOKEN: REFRESH_TOKEN, - CONF_USER_ID: USER_ID, - }, - USERNAME, - ), - ( - { - CONF_TOKEN: REFRESH_TOKEN, - CONF_USER_ID: USER_ID, - }, - USER_ID, - ), - ], -) -async def test_step_reauth_from_scratch( - hass, config, config_entry, credentials_config, setup_simplisafe, sms_config -): - """Test the re-auth step when a complete redo is needed.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USERNAME: "12345", CONF_TOKEN: "token123"}, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.unique_id == USER_ID - assert config_entry.data == { - CONF_TOKEN: REFRESH_TOKEN, - CONF_USERNAME: USERNAME, - } + assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"} -@pytest.mark.parametrize( - "exc,error_string", - [(InvalidCredentialsError, "invalid_auth"), (SimplipyError, "unknown")], -) -async def test_step_user_errors(hass, credentials_config, error_string, exc): - """Test that errors during the user step are handled.""" +@pytest.mark.parametrize("unique_id", ["some_other_id"]) +async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe): + """Test the re-auth step where the wrong account is used during login.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USERNAME: "12345", CONF_TOKEN: "token123"}, + ) + assert result["step_id"] == "user" + with patch( - "homeassistant.components.simplisafe.API.async_from_credentials", - side_effect=exc, + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "wrong_account" + + +async def test_step_user(hass, setup_simplisafe): + """Test the user step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + assert len(hass.config_entries.async_entries()) == 1 + [config_entry] = hass.config_entries.async_entries(DOMAIN) + assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"} + + +async def test_unknown_error(hass, setup_simplisafe): + """Test that an unknown error shows ohe correct error.""" + with patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + side_effect=SimplipyError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": error_string} - - -@pytest.mark.parametrize("api_auth_state", [AuthStates.PENDING_2FA_EMAIL]) -async def test_step_user_email_2fa( - api, api_auth_state, hass, config, credentials_config, setup_simplisafe -): - """Test the user step with email-based 2FA.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - # Patch API.async_verify_2fa_email to first return pending, then return all done: - api.async_verify_2fa_email.side_effect = [Verify2FAPending, None] - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert len(hass.config_entries.async_entries()) == 1 - [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.unique_id == USER_ID - assert config_entry.data == config - - -@patch("homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_TIMEOUT", 0) -@pytest.mark.parametrize("api_auth_state", [AuthStates.PENDING_2FA_EMAIL]) -async def test_step_user_email_2fa_timeout( - api, hass, config, credentials_config, setup_simplisafe -): - """Test a timeout during the user step with email-based 2FA.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - # Patch API.async_verify_2fa_email to return pending: - api.async_verify_2fa_email.side_effect = Verify2FAPending - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE - assert result["step_id"] == "email_2fa_error" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "email_2fa_timed_out" - - -async def test_step_user_sms_2fa( - hass, config, credentials_config, setup_simplisafe, sms_config -): - """Test the user step with SMS-based 2FA.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - - assert len(hass.config_entries.async_entries()) == 1 - [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.unique_id == USER_ID - assert config_entry.data == config - - -@pytest.mark.parametrize( - "exc,error_string", [(InvalidCredentialsError, "invalid_auth")] -) -async def test_step_user_sms_2fa_errors( - api, - hass, - config, - credentials_config, - error_string, - exc, - setup_simplisafe, - sms_config, -): - """Test that errors during the SMS-based 2FA step are handled.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - # Simulate entering the incorrect SMS code: - api.async_verify_2fa_sms.side_effect = InvalidCredentialsError - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"code": error_string} + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} From c9ae409d9a5d15ba78d12af0d88a55b456e5b430 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 Jul 2022 22:38:09 +0200 Subject: [PATCH 669/820] Update sentry-sdk to 1.8.0 (#75691) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index c1163ea1791..f6567fbd04d 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.7.2"], + "requirements": ["sentry-sdk==1.8.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index fff6dd9a48f..7194370ca6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2170,7 +2170,7 @@ sense_energy==0.10.4 sensorpush-ble==1.4.2 # homeassistant.components.sentry -sentry-sdk==1.7.2 +sentry-sdk==1.8.0 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b485db528ef..0edf582d77d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1454,7 +1454,7 @@ sense_energy==0.10.4 sensorpush-ble==1.4.2 # homeassistant.components.sentry -sentry-sdk==1.7.2 +sentry-sdk==1.8.0 # homeassistant.components.sharkiq sharkiq==0.0.1 From d890598da7003fdc4ec0b33ebf761824fe3ee661 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 16:38:07 -0500 Subject: [PATCH 670/820] Update PySwitchbot to improve connection reliability (#75692) --- .../components/switchbot/__init__.py | 35 ++++--------------- .../components/switchbot/config_flow.py | 22 ++---------- homeassistant/components/switchbot/const.py | 6 +--- .../components/switchbot/coordinator.py | 10 ------ .../components/switchbot/manifest.json | 2 +- .../components/switchbot/strings.json | 3 +- .../components/switchbot/translations/en.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/switchbot/test_config_flow.py | 11 +----- 10 files changed, 16 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 2dbc66a864d..46d6755553a 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -1,9 +1,6 @@ """Support for Switchbot devices.""" -from collections.abc import Mapping import logging -from types import MappingProxyType -from typing import Any import switchbot @@ -24,11 +21,8 @@ from .const import ( ATTR_BOT, ATTR_CURTAIN, ATTR_HYGROMETER, - COMMON_OPTIONS, CONF_RETRY_COUNT, - CONF_RETRY_TIMEOUT, DEFAULT_RETRY_COUNT, - DEFAULT_RETRY_TIMEOUT, DOMAIN, ) from .coordinator import SwitchbotDataUpdateCoordinator @@ -49,8 +43,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Switchbot from a config entry.""" hass.data.setdefault(DOMAIN, {}) - domain_data = hass.data[DOMAIN] - if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data: # Bleak uses addresses not mac addresses which are are actually # UUIDs on some platforms (MacOS). @@ -65,10 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.options: hass.config_entries.async_update_entry( entry, - options={ - CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT, - CONF_RETRY_TIMEOUT: DEFAULT_RETRY_TIMEOUT, - }, + options={CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT}, ) sensor_type: str = entry.data[CONF_SENSOR_TYPE] @@ -78,13 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Could not find Switchbot {sensor_type} with address {address}" ) - - if COMMON_OPTIONS not in domain_data: - domain_data[COMMON_OPTIONS] = entry.options - - common_options: Mapping[str, int] = domain_data[COMMON_OPTIONS] - switchbot.DEFAULT_RETRY_TIMEOUT = common_options[CONF_RETRY_TIMEOUT] - cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) device = cls( device=ble_device, @@ -92,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: retry_count=entry.options[CONF_RETRY_COUNT], ) coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator( - hass, _LOGGER, ble_device, device, common_options + hass, _LOGGER, ble_device, device ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): @@ -106,6 +88,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" sensor_type = entry.data[CONF_SENSOR_TYPE] @@ -119,11 +106,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.pop(DOMAIN) return unload_ok - - -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - # Update entity options stored in hass. - common_options: MappingProxyType[str, Any] = hass.data[DOMAIN][COMMON_OPTIONS] - if entry.options != common_options: - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index f6f175819b6..3a34a89d9fd 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -17,14 +17,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo -from .const import ( - CONF_RETRY_COUNT, - CONF_RETRY_TIMEOUT, - DEFAULT_RETRY_COUNT, - DEFAULT_RETRY_TIMEOUT, - DOMAIN, - SUPPORTED_MODEL_TYPES, -) +from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES _LOGGER = logging.getLogger(__name__) @@ -140,11 +133,6 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): """Manage Switchbot options.""" if user_input is not None: # Update common entity options for all other entities. - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id != self.config_entry.unique_id: - self.hass.config_entries.async_update_entry( - entry, options=user_input - ) return self.async_create_entry(title="", data=user_input) options = { @@ -153,13 +141,7 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): default=self.config_entry.options.get( CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT ), - ): int, - vol.Optional( - CONF_RETRY_TIMEOUT, - default=self.config_entry.options.get( - CONF_RETRY_TIMEOUT, DEFAULT_RETRY_TIMEOUT - ), - ): int, + ): int } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index a841b8388dd..e9602a19048 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -15,15 +15,11 @@ SUPPORTED_MODEL_TYPES = { # Config Defaults DEFAULT_RETRY_COUNT = 3 -DEFAULT_RETRY_TIMEOUT = 5 # Config Options CONF_RETRY_COUNT = "retry_count" -CONF_SCAN_TIMEOUT = "scan_timeout" # Deprecated config Entry Options to be removed in 2023.4 CONF_TIME_BETWEEN_UPDATE_COMMAND = "update_time" CONF_RETRY_TIMEOUT = "retry_timeout" - -# Data -COMMON_OPTIONS = "common_options" +CONF_SCAN_TIMEOUT = "scan_timeout" diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 74cc54402d1..31f7f2d3992 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Mapping import logging from typing import Any, cast @@ -16,8 +15,6 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( ) from homeassistant.core import HomeAssistant, callback -from .const import CONF_RETRY_COUNT - _LOGGER = logging.getLogger(__name__) @@ -38,21 +35,14 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): logger: logging.Logger, ble_device: BLEDevice, device: switchbot.SwitchbotDevice, - common_options: Mapping[str, int], ) -> None: """Initialize global switchbot data updater.""" super().__init__(hass, logger, ble_device.address) self.ble_device = ble_device self.device = device - self.common_options = common_options self.data: dict[str, Any] = {} self._ready_event = asyncio.Event() - @property - def retry_count(self) -> int: - """Return retry count.""" - return self.common_options[CONF_RETRY_COUNT] - @callback def _async_handle_bluetooth_event( self, diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index c23891083a4..a82ef264698 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.15.0"], + "requirements": ["PySwitchbot==0.15.1"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": ["@danielhiversen", "@RenierM26", "@murtas"], diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index f0758d767aa..797d1d7613c 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -24,8 +24,7 @@ "step": { "init": { "data": { - "retry_count": "Retry count", - "retry_timeout": "Timeout between retries" + "retry_count": "Retry count" } } } diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 15127b82101..0407f71ece3 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -24,8 +24,7 @@ "step": { "init": { "data": { - "retry_count": "Retry count", - "retry_timeout": "Timeout between retries" + "retry_count": "Retry count" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 7194370ca6a..3a39b405c91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.0 +PySwitchbot==0.15.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0edf582d77d..e9f699d61d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.0 +PySwitchbot==0.15.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index b9d1d556b09..0ae3430eeb1 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -2,10 +2,7 @@ from unittest.mock import patch -from homeassistant.components.switchbot.const import ( - CONF_RETRY_COUNT, - CONF_RETRY_TIMEOUT, -) +from homeassistant.components.switchbot.const import CONF_RETRY_COUNT from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.data_entry_flow import FlowResultType @@ -276,7 +273,6 @@ async def test_options_flow(hass): }, options={ CONF_RETRY_COUNT: 10, - CONF_RETRY_TIMEOUT: 10, }, unique_id="aabbccddeeff", ) @@ -294,14 +290,12 @@ async def test_options_flow(hass): result["flow_id"], user_input={ CONF_RETRY_COUNT: 3, - CONF_RETRY_TIMEOUT: 5, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 3 - assert result["data"][CONF_RETRY_TIMEOUT] == 5 assert len(mock_setup_entry.mock_calls) == 2 @@ -319,16 +313,13 @@ async def test_options_flow(hass): result["flow_id"], user_input={ CONF_RETRY_COUNT: 6, - CONF_RETRY_TIMEOUT: 6, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 6 - assert result["data"][CONF_RETRY_TIMEOUT] == 6 assert len(mock_setup_entry.mock_calls) == 1 assert entry.options[CONF_RETRY_COUNT] == 6 - assert entry.options[CONF_RETRY_TIMEOUT] == 6 From bbb9443b00693a0bad2e6cfbe789baa5dc5542da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 16:39:53 -0500 Subject: [PATCH 671/820] Fix bluetooth integration matching with service_data_uuids and service_uuids (#75687) * Fix bluetooth integration with service_data and service_uuids We would only dispatch a new flow when the address was seen for the first time or the manufacturer_data appeared in a followup advertisement. Its also possible for the service_data and service_uuids to appear in a followup advertisement so we need to track these as well * improve logging to avoid overly large messages * improve logging to avoid overly large messages * adjust * adjsut * split * coverage * coverage * coverage * coverage * fix matcher * more coverage * more coverage * more coverage * revert switchbot changes and move to seperate PR --- .../components/bluetooth/__init__.py | 117 ++------- homeassistant/components/bluetooth/match.py | 139 ++++++++++ homeassistant/loader.py | 1 + script/hassfest/manifest.py | 1 + tests/components/bluetooth/test_init.py | 243 +++++++++++++++++- 5 files changed, 398 insertions(+), 103 deletions(-) create mode 100644 homeassistant/components/bluetooth/match.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 901b8ee0644..fb514a33e99 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -5,15 +5,13 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum -import fnmatch import logging import platform -from typing import Final, TypedDict, Union +from typing import Final, Union from bleak import BleakError from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from lru import LRU # pylint: disable=no-name-in-module from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -28,20 +26,21 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import ( - BluetoothMatcher, - BluetoothMatcherOptional, - async_get_bluetooth, -) +from homeassistant.loader import async_get_bluetooth from . import models from .const import DOMAIN +from .match import ( + ADDRESS, + BluetoothCallbackMatcher, + IntegrationMatcher, + ble_device_matches, +) from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher _LOGGER = logging.getLogger(__name__) -MAX_REMEMBER_ADDRESSES: Final = 2048 UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 @@ -79,19 +78,6 @@ class BluetoothServiceInfoBleak(BluetoothServiceInfo): ) -class BluetoothCallbackMatcherOptional(TypedDict, total=False): - """Matcher for the bluetooth integration for callback optional fields.""" - - address: str - - -class BluetoothCallbackMatcher( - BluetoothMatcherOptional, - BluetoothCallbackMatcherOptional, -): - """Callback matcher for the bluetooth integration.""" - - class BluetoothScanningMode(Enum): """The mode of scanning for bluetooth devices.""" @@ -104,12 +90,6 @@ SCANNING_MODE_TO_BLEAK = { BluetoothScanningMode.PASSIVE: "passive", } -ADDRESS: Final = "address" -LOCAL_NAME: Final = "local_name" -SERVICE_UUID: Final = "service_uuid" -MANUFACTURER_ID: Final = "manufacturer_id" -MANUFACTURER_DATA_START: Final = "manufacturer_data_start" - BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[ @@ -208,8 +188,8 @@ async def _async_has_bluetooth_adapter() -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" - integration_matchers = await async_get_bluetooth(hass) - manager = BluetoothManager(hass, integration_matchers) + integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) + manager = BluetoothManager(hass, integration_matcher) manager.async_setup() hass.data[DOMAIN] = manager # The config entry is responsible for starting the manager @@ -252,62 +232,17 @@ async def async_unload_entry( return True -def _ble_device_matches( - matcher: BluetoothCallbackMatcher | BluetoothMatcher, - device: BLEDevice, - advertisement_data: AdvertisementData, -) -> bool: - """Check if a ble device and advertisement_data matches the matcher.""" - if ( - matcher_address := matcher.get(ADDRESS) - ) is not None and device.address != matcher_address: - return False - - if ( - matcher_local_name := matcher.get(LOCAL_NAME) - ) is not None and not fnmatch.fnmatch( - advertisement_data.local_name or device.name or device.address, - matcher_local_name, - ): - return False - - if ( - matcher_service_uuid := matcher.get(SERVICE_UUID) - ) is not None and matcher_service_uuid not in advertisement_data.service_uuids: - return False - - if ( - (matcher_manfacturer_id := matcher.get(MANUFACTURER_ID)) is not None - and matcher_manfacturer_id not in advertisement_data.manufacturer_data - ): - return False - - if ( - matcher_manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START) - ) is not None: - matcher_manufacturer_data_start_bytes = bytearray( - matcher_manufacturer_data_start - ) - if not any( - manufacturer_data.startswith(matcher_manufacturer_data_start_bytes) - for manufacturer_data in advertisement_data.manufacturer_data.values() - ): - return False - - return True - - class BluetoothManager: """Manage Bluetooth.""" def __init__( self, hass: HomeAssistant, - integration_matchers: list[BluetoothMatcher], + integration_matcher: IntegrationMatcher, ) -> None: """Init bluetooth discovery.""" self.hass = hass - self._integration_matchers = integration_matchers + self._integration_matcher = integration_matcher self.scanner: HaBleakScanner | None = None self._cancel_device_detected: CALLBACK_TYPE | None = None self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None @@ -315,9 +250,6 @@ class BluetoothManager: self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] - # Some devices use a random address so we need to use - # an LRU to avoid memory issues. - self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES) @hass_callback def async_setup(self) -> None: @@ -387,27 +319,12 @@ class BluetoothManager: self, device: BLEDevice, advertisement_data: AdvertisementData ) -> None: """Handle a detected device.""" - matched_domains: set[str] | None = None - match_key = (device.address, bool(advertisement_data.manufacturer_data)) - match_key_has_mfr_data = (device.address, True) - - # If we matched without manufacturer_data, we need to do it again - # since we may think the device is unsupported otherwise - if ( - match_key_has_mfr_data not in self._matched - and match_key not in self._matched - ): - matched_domains = { - matcher["domain"] - for matcher in self._integration_matchers - if _ble_device_matches(matcher, device, advertisement_data) - } - if matched_domains: - self._matched[match_key] = True - + matched_domains = self._integration_matcher.match_domains( + device, advertisement_data + ) _LOGGER.debug( "Device detected: %s with advertisement_data: %s matched domains: %s", - device, + device.address, advertisement_data, matched_domains, ) @@ -417,7 +334,7 @@ class BluetoothManager: service_info: BluetoothServiceInfoBleak | None = None for callback, matcher in self._callbacks: - if matcher is None or _ble_device_matches( + if matcher is None or ble_device_matches( matcher, device, advertisement_data ): if service_info is None: diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py new file mode 100644 index 00000000000..c4560287feb --- /dev/null +++ b/homeassistant/components/bluetooth/match.py @@ -0,0 +1,139 @@ +"""The bluetooth integration matchers.""" +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +import fnmatch +from typing import Final, TypedDict + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional + +MAX_REMEMBER_ADDRESSES: Final = 2048 + + +ADDRESS: Final = "address" +LOCAL_NAME: Final = "local_name" +SERVICE_UUID: Final = "service_uuid" +SERVICE_DATA_UUID: Final = "service_data_uuid" +MANUFACTURER_ID: Final = "manufacturer_id" +MANUFACTURER_DATA_START: Final = "manufacturer_data_start" + + +class BluetoothCallbackMatcherOptional(TypedDict, total=False): + """Matcher for the bluetooth integration for callback optional fields.""" + + address: str + + +class BluetoothCallbackMatcher( + BluetoothMatcherOptional, + BluetoothCallbackMatcherOptional, +): + """Callback matcher for the bluetooth integration.""" + + +@dataclass(frozen=False) +class IntegrationMatchHistory: + """Track which fields have been seen.""" + + manufacturer_data: bool + service_data: bool + service_uuids: bool + + +def seen_all_fields( + previous_match: IntegrationMatchHistory, adv_data: AdvertisementData +) -> bool: + """Return if we have seen all fields.""" + if not previous_match.manufacturer_data and adv_data.manufacturer_data: + return False + if not previous_match.service_data and adv_data.service_data: + return False + if not previous_match.service_uuids and adv_data.service_uuids: + return False + return True + + +class IntegrationMatcher: + """Integration matcher for the bluetooth integration.""" + + def __init__(self, integration_matchers: list[BluetoothMatcher]) -> None: + """Initialize the matcher.""" + self._integration_matchers = integration_matchers + # Some devices use a random address so we need to use + # an LRU to avoid memory issues. + self._matched: Mapping[str, IntegrationMatchHistory] = LRU( + MAX_REMEMBER_ADDRESSES + ) + + def match_domains(self, device: BLEDevice, adv_data: AdvertisementData) -> set[str]: + """Return the domains that are matched.""" + matched_domains: set[str] = set() + if (previous_match := self._matched.get(device.address)) and seen_all_fields( + previous_match, adv_data + ): + # We have seen all fields so we can skip the rest of the matchers + return matched_domains + matched_domains = { + matcher["domain"] + for matcher in self._integration_matchers + if ble_device_matches(matcher, device, adv_data) + } + if not matched_domains: + return matched_domains + if previous_match: + previous_match.manufacturer_data |= bool(adv_data.manufacturer_data) + previous_match.service_data |= bool(adv_data.service_data) + previous_match.service_uuids |= bool(adv_data.service_uuids) + else: + self._matched[device.address] = IntegrationMatchHistory( # type: ignore[index] + manufacturer_data=bool(adv_data.manufacturer_data), + service_data=bool(adv_data.service_data), + service_uuids=bool(adv_data.service_uuids), + ) + return matched_domains + + +def ble_device_matches( + matcher: BluetoothCallbackMatcher | BluetoothMatcher, + device: BLEDevice, + adv_data: AdvertisementData, +) -> bool: + """Check if a ble device and advertisement_data matches the matcher.""" + if (address := matcher.get(ADDRESS)) is not None and device.address != address: + return False + + if (local_name := matcher.get(LOCAL_NAME)) is not None and not fnmatch.fnmatch( + adv_data.local_name or device.name or device.address, + local_name, + ): + return False + + if ( + service_uuid := matcher.get(SERVICE_UUID) + ) is not None and service_uuid not in adv_data.service_uuids: + return False + + if ( + service_data_uuid := matcher.get(SERVICE_DATA_UUID) + ) is not None and service_data_uuid not in adv_data.service_data: + return False + + if ( + manfacturer_id := matcher.get(MANUFACTURER_ID) + ) is not None and manfacturer_id not in adv_data.manufacturer_data: + return False + + if (manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START)) is not None: + manufacturer_data_start_bytes = bytearray(manufacturer_data_start) + if not any( + manufacturer_data.startswith(manufacturer_data_start_bytes) + for manufacturer_data in adv_data.manufacturer_data.values() + ): + return False + + return True diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 9de06c48786..e4aff23d3f1 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -88,6 +88,7 @@ class BluetoothMatcherOptional(TypedDict, total=False): local_name: str service_uuid: str + service_data_uuid: str manufacturer_id: int manufacturer_data_start: list[int] diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 129899cff11..c6cc019ff31 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -194,6 +194,7 @@ MANIFEST_SCHEMA = vol.Schema( vol.Schema( { vol.Optional("service_uuid"): vol.All(str, verify_lowercase), + vol.Optional("service_data_uuid"): vol.All(str, verify_lowercase), vol.Optional("local_name"): vol.All(str), vol.Optional("manufacturer_id"): int, vol.Optional("manufacturer_data_start"): [int], diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 6dbec251026..8b0d60bc42d 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -222,7 +222,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): assert mock_config_flow.mock_calls[0][1][0] == "switchbot" -async def test_discovery_match_by_manufacturer_id_and_first_byte( +async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( hass, mock_bleak_scanner_start ): """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start.""" @@ -248,20 +248,33 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( assert len(mock_bleak_scanner_start.mock_calls) == 1 hkc_device = BLEDevice("44:44:33:11:23:45", "lock") + hkc_adv_no_mfr_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={}, + ) hkc_adv = AdvertisementData( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x06\x02\x03\x99"}, ) + # 1st discovery with no manufacturer data + # should not trigger config flow + _get_underlying_scanner()._callback(hkc_device, hkc_adv_no_mfr_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 2nd discovery with manufacturer data + # should trigger a config flow _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() - assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller" mock_config_flow.reset_mock() - # 2nd discovery should not generate another flow + # 3rd discovery should not generate another flow _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() @@ -288,6 +301,230 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( assert len(mock_config_flow.mock_calls) == 0 +async def test_discovery_match_by_service_data_uuid_then_others( + hass, mock_bleak_scanner_start +): + """Test bluetooth discovery match by service_data_uuid and then other fields.""" + mock_bt = [ + { + "domain": "my_domain", + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb", + }, + { + "domain": "my_domain", + "service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc", + }, + { + "domain": "other_domain", + "manufacturer_id": 323, + }, + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + device = BLEDevice("44:44:33:11:23:45", "lock") + adv_without_service_data_uuid = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={}, + ) + adv_with_mfr_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={323: b"\x01\x02\x03"}, + service_data={}, + ) + adv_with_service_data_uuid = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"}, + ) + adv_with_service_data_uuid_and_mfr_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={323: b"\x01\x02\x03"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"}, + ) + adv_with_service_data_uuid_and_mfr_data_and_service_uuid = AdvertisementData( + local_name="lock", + manufacturer_data={323: b"\x01\x02\x03"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"}, + service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"], + ) + adv_with_service_uuid = AdvertisementData( + local_name="lock", + manufacturer_data={}, + service_data={}, + service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"], + ) + # 1st discovery should not generate a flow because the + # service_data_uuid is not in the advertisement + _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 2nd discovery should not generate a flow because the + # service_data_uuid is not in the advertisement + _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 3rd discovery should generate a flow because the + # manufacturer_data is in the advertisement + _get_underlying_scanner()._callback(device, adv_with_mfr_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "other_domain" + mock_config_flow.reset_mock() + + # 4th discovery should generate a flow because the + # service_data_uuid is in the advertisement and + # we never saw a service_data_uuid before + _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "my_domain" + mock_config_flow.reset_mock() + + # 5th discovery should not generate a flow because the + # we already saw an advertisement with the service_data_uuid + _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 6th discovery should not generate a flow because the + # manufacturer_data is in the advertisement + # and we saw manufacturer_data before + _get_underlying_scanner()._callback( + device, adv_with_service_data_uuid_and_mfr_data + ) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 7th discovery should generate a flow because the + # service_uuids is in the advertisement + # and we never saw service_uuids before + _get_underlying_scanner()._callback( + device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid + ) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 2 + assert { + mock_config_flow.mock_calls[0][1][0], + mock_config_flow.mock_calls[1][1][0], + } == {"my_domain", "other_domain"} + mock_config_flow.reset_mock() + + # 8th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback( + device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid + ) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 9th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback(device, adv_with_service_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 10th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 11th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + +async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( + hass, mock_bleak_scanner_start +): + """Test bluetooth discovery matches twice for service_uuid and then manufacturer_id.""" + mock_bt = [ + { + "domain": "my_domain", + "manufacturer_id": 76, + }, + { + "domain": "my_domain", + "service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc", + }, + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + device = BLEDevice("44:44:33:11:23:45", "lock") + adv_service_uuids = AdvertisementData( + local_name="lock", + service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fc"], + manufacturer_data={}, + ) + adv_manufacturer_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={76: b"\x06\x02\x03\x99"}, + ) + + # 1st discovery with matches service_uuid + # should trigger config flow + _get_underlying_scanner()._callback(device, adv_service_uuids) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "my_domain" + mock_config_flow.reset_mock() + + # 2nd discovery with manufacturer data + # should trigger a config flow + _get_underlying_scanner()._callback(device, adv_manufacturer_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "my_domain" + mock_config_flow.reset_mock() + + # 3rd discovery should not generate another flow + _get_underlying_scanner()._callback(device, adv_service_uuids) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 4th discovery should not generate another flow + _get_underlying_scanner()._callback(device, adv_manufacturer_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): """Test the async_discovered_device API.""" mock_bt = [] From 511af3c455d8dd1779fbe1f6127e0b6d258a5bcb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 16:55:52 -0500 Subject: [PATCH 672/820] Update switchbot bluetooth matchers for sensor devices (#75690) --- CODEOWNERS | 4 ++-- homeassistant/components/switchbot/manifest.json | 11 +++++++++-- homeassistant/generated/bluetooth.py | 4 ++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 66e54b25b9e..bcbd6c20364 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1042,8 +1042,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core -/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas -/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas +/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas +/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode /homeassistant/components/switchmate/ @danielhiversen diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index a82ef264698..3bccbb4f674 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -5,8 +5,15 @@ "requirements": ["PySwitchbot==0.15.1"], "config_flow": true, "dependencies": ["bluetooth"], - "codeowners": ["@danielhiversen", "@RenierM26", "@murtas"], - "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], + "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], + "bluetooth": [ + { + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" + }, + { + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + } + ], "iot_class": "local_push", "loggers": ["switchbot"] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index f9dd4352e28..5dde90f1f7a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -65,6 +65,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "sensorpush", "local_name": "SensorPush*" }, + { + "domain": "switchbot", + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" + }, { "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" From 22ca28b93d4c88eda928b4e22b2d5510a369821c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 19:23:23 -0500 Subject: [PATCH 673/820] Ensure bluetooth can be reloaded when hot plugging a bluetooth adapter (#75699) --- homeassistant/components/bluetooth/__init__.py | 8 +++++++- tests/components/bluetooth/test_init.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index fb514a33e99..9266603839c 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -440,5 +440,11 @@ class BluetoothManager: self._cancel_unavailable_tracking() self._cancel_unavailable_tracking = None if self.scanner: - await self.scanner.stop() + try: + await self.scanner.stop() + except BleakError as ex: + # This is not fatal, and they may want to reload + # the config entry to restart the scanner if they + # change the bluetooth dongle. + _LOGGER.error("Error stopping scanner: %s", ex) uninstall_multiple_bleak_catcher() diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 8b0d60bc42d..aa3a6253b83 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1213,3 +1213,20 @@ async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_blu """Test getting the scanner returns the wrapped instance.""" scanner = bluetooth.async_get_scanner(hass) assert isinstance(scanner, models.HaBleakScannerWrapper) + + +async def test_config_entry_can_be_reloaded_when_stop_raises( + hass, caplog, enable_bluetooth +): + """Test we can reload if stopping the scanner raises.""" + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop", side_effect=BleakError + ): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert "Error stopping scanner" in caplog.text From 4a50010458e3552cba4e617eb827839587a9be79 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 25 Jul 2022 00:25:55 +0000 Subject: [PATCH 674/820] [ci skip] Translation update --- .../components/bluetooth/translations/et.json | 22 ++++++++++++ .../components/bluetooth/translations/id.json | 6 ++++ .../components/bluetooth/translations/it.json | 6 ++++ .../components/bluetooth/translations/ja.json | 6 ++++ .../bluetooth/translations/pt-BR.json | 6 ++++ .../bluetooth/translations/zh-Hant.json | 6 ++++ .../components/demo/translations/it.json | 16 +++++++++ .../components/generic/translations/it.json | 4 ++- .../components/google/translations/it.json | 3 +- .../components/govee_ble/translations/ca.json | 21 +++++++++++ .../components/govee_ble/translations/de.json | 21 +++++++++++ .../components/govee_ble/translations/el.json | 21 +++++++++++ .../components/govee_ble/translations/fr.json | 21 +++++++++++ .../components/govee_ble/translations/id.json | 21 +++++++++++ .../components/govee_ble/translations/it.json | 21 +++++++++++ .../components/govee_ble/translations/ja.json | 21 +++++++++++ .../govee_ble/translations/pt-BR.json | 21 +++++++++++ .../here_travel_time/translations/it.json | 10 ++++-- .../components/homekit/translations/it.json | 4 +-- .../components/inkbird/translations/et.json | 21 +++++++++++ .../components/inkbird/translations/it.json | 21 +++++++++++ .../components/lifx/translations/it.json | 8 +++++ .../components/moat/translations/ca.json | 21 +++++++++++ .../components/moat/translations/de.json | 21 +++++++++++ .../components/moat/translations/el.json | 21 +++++++++++ .../components/moat/translations/fr.json | 21 +++++++++++ .../components/moat/translations/id.json | 21 +++++++++++ .../components/moat/translations/it.json | 21 +++++++++++ .../components/moat/translations/ja.json | 21 +++++++++++ .../components/moat/translations/pt-BR.json | 21 +++++++++++ .../components/nextdns/translations/it.json | 15 ++++++++ .../components/plugwise/translations/it.json | 3 +- .../components/rhasspy/translations/it.json | 3 ++ .../sensorpush/translations/et.json | 21 +++++++++++ .../sensorpush/translations/it.json | 21 +++++++++++ .../simplisafe/translations/de.json | 7 ++-- .../simplisafe/translations/en.json | 21 ++++++++++- .../simplisafe/translations/fr.json | 8 +++-- .../simplisafe/translations/it.json | 7 ++-- .../simplisafe/translations/pt-BR.json | 7 ++-- .../components/switchbot/translations/de.json | 1 + .../components/switchbot/translations/en.json | 7 ++-- .../components/switchbot/translations/fr.json | 3 +- .../components/switchbot/translations/it.json | 3 +- .../switchbot/translations/pt-BR.json | 3 +- .../twentemilieu/translations/de.json | 2 +- .../components/uscis/translations/it.json | 2 +- .../components/verisure/translations/it.json | 6 ++-- .../components/withings/translations/it.json | 3 +- .../xiaomi_ble/translations/de.json | 9 +++++ .../xiaomi_ble/translations/en.json | 15 ++++++++ .../xiaomi_ble/translations/et.json | 21 +++++++++++ .../xiaomi_ble/translations/id.json | 16 ++++++++- .../xiaomi_ble/translations/it.json | 36 +++++++++++++++++++ .../xiaomi_ble/translations/ja.json | 21 +++++++++++ .../xiaomi_ble/translations/pt-BR.json | 36 +++++++++++++++++++ .../xiaomi_ble/translations/zh-Hant.json | 21 +++++++++++ .../components/zha/translations/et.json | 1 + .../components/zha/translations/it.json | 1 + .../components/zha/translations/ja.json | 1 + 60 files changed, 767 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/bluetooth/translations/et.json create mode 100644 homeassistant/components/govee_ble/translations/ca.json create mode 100644 homeassistant/components/govee_ble/translations/de.json create mode 100644 homeassistant/components/govee_ble/translations/el.json create mode 100644 homeassistant/components/govee_ble/translations/fr.json create mode 100644 homeassistant/components/govee_ble/translations/id.json create mode 100644 homeassistant/components/govee_ble/translations/it.json create mode 100644 homeassistant/components/govee_ble/translations/ja.json create mode 100644 homeassistant/components/govee_ble/translations/pt-BR.json create mode 100644 homeassistant/components/inkbird/translations/et.json create mode 100644 homeassistant/components/inkbird/translations/it.json create mode 100644 homeassistant/components/moat/translations/ca.json create mode 100644 homeassistant/components/moat/translations/de.json create mode 100644 homeassistant/components/moat/translations/el.json create mode 100644 homeassistant/components/moat/translations/fr.json create mode 100644 homeassistant/components/moat/translations/id.json create mode 100644 homeassistant/components/moat/translations/it.json create mode 100644 homeassistant/components/moat/translations/ja.json create mode 100644 homeassistant/components/moat/translations/pt-BR.json create mode 100644 homeassistant/components/sensorpush/translations/et.json create mode 100644 homeassistant/components/sensorpush/translations/it.json create mode 100644 homeassistant/components/xiaomi_ble/translations/et.json create mode 100644 homeassistant/components/xiaomi_ble/translations/it.json create mode 100644 homeassistant/components/xiaomi_ble/translations/ja.json create mode 100644 homeassistant/components/xiaomi_ble/translations/pt-BR.json create mode 100644 homeassistant/components/xiaomi_ble/translations/zh-Hant.json diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json new file mode 100644 index 00000000000..4098abefbf7 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name} ?" + }, + "enable_bluetooth": { + "description": "Kas soovid Bluetoothi seadistada?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index 12653696241..5fe99b68c2f 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Ingin menyiapkan {name}?" }, + "enable_bluetooth": { + "description": "Ingin menyiapkan Bluetooth?" + }, "user": { "data": { "address": "Perangkat" diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index 12d79b3efff..af83bfd271d 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Vuoi configurare {name}?" }, + "enable_bluetooth": { + "description": "Vuoi configurare il Bluetooth?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index f42ea56e2c8..6257d1c67d4 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, + "enable_bluetooth": { + "description": "Bluetooth\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index 9cafa844652..05ddee20ecb 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Deseja configurar {name}?" }, + "enable_bluetooth": { + "description": "Deseja configurar o Bluetooth?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index 3f92640469e..7d69eb15ac1 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "enable_bluetooth": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u82bd\uff1f" + }, "user": { "data": { "address": "\u88dd\u7f6e" diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 7fc00caf26e..80281a62703 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -1,6 +1,22 @@ { "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Premere OK quando il liquido delle frecce \u00e8 stato riempito", + "title": "Il liquido delle frecce deve essere rabboccato" + } + } + }, + "title": "Il liquido delle frecce \u00e8 vuoto e deve essere rabboccato" + }, + "transmogrifier_deprecated": { + "description": "Il componente transmogrifier \u00e8 ora deprecato a causa della mancanza di controllo locale disponibile nella nuova API", + "title": "Il componente transmogrifier \u00e8 deprecato" + }, "unfixable_problem": { + "description": "Questo problema non si risolver\u00e0 mai.", "title": "Questo non \u00e8 un problema risolvibile" } }, diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 80f8c90ce16..14d4b6e8720 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -7,7 +7,7 @@ "error": { "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", - "malformed_url": "URL malformato", + "malformed_url": "URL non valido", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", "relative_url": "Non sono consentiti URL relativi", "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", @@ -52,7 +52,9 @@ "error": { "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", + "malformed_url": "URL non valido", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", + "relative_url": "Non sono consentiti URL relativi", "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index ef5ec01202d..782fed55d5b 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token di accesso non valido", "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "oauth_error": "Ricevuti dati token non validi.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "timeout_connect": "Tempo scaduto per stabile la connessione." }, "create_entry": { "default": "Autenticazione riuscita" diff --git a/homeassistant/components/govee_ble/translations/ca.json b/homeassistant/components/govee_ble/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/govee_ble/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/de.json b/homeassistant/components/govee_ble/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/el.json b/homeassistant/components/govee_ble/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/fr.json b/homeassistant/components/govee_ble/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/govee_ble/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/id.json b/homeassistant/components/govee_ble/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/it.json b/homeassistant/components/govee_ble/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/ja.json b/homeassistant/components/govee_ble/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/pt-BR.json b/homeassistant/components/govee_ble/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/it.json b/homeassistant/components/here_travel_time/translations/it.json index 7c87ef36d0e..17cd09fed8b 100644 --- a/homeassistant/components/here_travel_time/translations/it.json +++ b/homeassistant/components/here_travel_time/translations/it.json @@ -22,8 +22,8 @@ }, "destination_menu": { "menu_options": { - "destination_coordinates": "Utilizzando una posizione sulla mappa", - "destination_entity": "Utilizzando un'entit\u00e0" + "destination_coordinates": "Utilizzo di una posizione sulla mappa", + "destination_entity": "Utilizzo di un'entit\u00e0" }, "title": "Scegli la destinazione" }, @@ -40,7 +40,11 @@ "title": "Scegli la partenza" }, "origin_menu": { - "title": "Scegli Origine" + "menu_options": { + "origin_coordinates": "Utilizzo di una posizione sulla mappa", + "origin_entity": "Utilizzo di un'entit\u00e0" + }, + "title": "Scegli la partenza" }, "user": { "data": { diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index c46acd8965d..3aefb01d63a 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -44,14 +44,14 @@ "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse ad eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", "title": "Seleziona le entit\u00e0 da escludere" }, "include": { "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a meno che non siano selezionate entit\u00e0 specifiche.", "title": "Seleziona le entit\u00e0 da includere" }, "init": { diff --git a/homeassistant/components/inkbird/translations/et.json b/homeassistant/components/inkbird/translations/et.json new file mode 100644 index 00000000000..749f7e45de5 --- /dev/null +++ b/homeassistant/components/inkbird/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/it.json b/homeassistant/components/inkbird/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/inkbird/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/it.json b/homeassistant/components/lifx/translations/it.json index d9c25e63590..9e8c090ad0d 100644 --- a/homeassistant/components/lifx/translations/it.json +++ b/homeassistant/components/lifx/translations/it.json @@ -1,9 +1,14 @@ { "config": { "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { @@ -18,6 +23,9 @@ } }, "user": { + "data": { + "host": "Host" + }, "description": "Se lasci l'host vuoto, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi." } } diff --git a/homeassistant/components/moat/translations/ca.json b/homeassistant/components/moat/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/moat/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/de.json b/homeassistant/components/moat/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/moat/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/el.json b/homeassistant/components/moat/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/moat/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/fr.json b/homeassistant/components/moat/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/moat/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/id.json b/homeassistant/components/moat/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/moat/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/it.json b/homeassistant/components/moat/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/moat/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/ja.json b/homeassistant/components/moat/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/moat/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/pt-BR.json b/homeassistant/components/moat/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/moat/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/it.json b/homeassistant/components/nextdns/translations/it.json index 67c9520f2d4..68d98f77d09 100644 --- a/homeassistant/components/nextdns/translations/it.json +++ b/homeassistant/components/nextdns/translations/it.json @@ -3,12 +3,27 @@ "abort": { "already_configured": "Questo profilo NextDNS \u00e8 gi\u00e0 configurato." }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "unknown": "Errore imprevisto" + }, "step": { "profiles": { "data": { "profile": "Profilo" } + }, + "user": { + "data": { + "api_key": "Chiave API" + } } } + }, + "system_health": { + "info": { + "can_reach_server": "Server raggiungibile" + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 24e75f3e846..e4bf239b7bc 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "anna_with_adam": "Sia Anna che Adam sono stati rilevati. Aggiungi il tuo Adamo al posto della tua Anna" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/rhasspy/translations/it.json b/homeassistant/components/rhasspy/translations/it.json index bd2daba89f6..55ce19aff24 100644 --- a/homeassistant/components/rhasspy/translations/it.json +++ b/homeassistant/components/rhasspy/translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, "step": { "user": { "description": "Vuoi abilitare il supporto Rhasspy?" diff --git a/homeassistant/components/sensorpush/translations/et.json b/homeassistant/components/sensorpush/translations/et.json new file mode 100644 index 00000000000..94bc17992fe --- /dev/null +++ b/homeassistant/components/sensorpush/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", + "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/it.json b/homeassistant/components/sensorpush/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index d224f3c2441..4788f2201b4 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet.", "email_2fa_timed_out": "Zeit\u00fcberschreitung beim Warten auf E-Mail-basierte Zwei-Faktor-Authentifizierung.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "wrong_account": "Die angegebenen Benutzeranmeldeinformationen stimmen nicht mit diesem SimpliSafe-Konto \u00fcberein." }, "error": { + "identifier_exists": "Konto bereits registriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Autorisierungscode", "password": "Passwort", "username": "Benutzername" }, - "description": "Gib deinen Benutzernamen und Passwort ein." + "description": "SimpliSafe authentifiziert Benutzer \u00fcber seine Web-App. Aufgrund technischer Einschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; Bitte stelle sicher, dass Du die [Dokumentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) lesen, bevor Sie beginnen. \n\n Wenn Sie fertig sind, klicke [hier]({url}), um die SimpliSafe-Web-App zu \u00f6ffnen und Ihre Anmeldeinformationen einzugeben. Wenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gebe den Autorisierungscode von der SimpliSafe-Web-App-URL ein." } } }, diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 82320df4864..70b0cc15383 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", + "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, @@ -10,10 +11,28 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, + "progress": { + "email_2fa": "Check your email for a verification link from Simplisafe." + }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please re-enter the password for {username}.", + "title": "Reauthenticate Integration" + }, + "sms_2fa": { + "data": { + "code": "Code" + }, + "description": "Input the two-factor authentication code sent to you via SMS." + }, "user": { "data": { - "auth_code": "Authorization Code" + "auth_code": "Authorization Code", + "password": "Password", + "username": "Username" }, "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL." } diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 4de2af95885..15ad31ce463 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9.", "email_2fa_timed_out": "D\u00e9lai d'attente de l'authentification \u00e0 deux facteurs par courriel expir\u00e9.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "wrong_account": "Les informations d'identification d'utilisateur fournies ne correspondent pas \u00e0 ce compte SimpliSafe." }, "error": { + "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, @@ -28,10 +30,10 @@ }, "user": { "data": { + "auth_code": "Code d'autorisation", "password": "Mot de passe", "username": "Nom d'utilisateur" - }, - "description": "Saisissez votre nom d'utilisateur et votre mot de passe." + } } } }, diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index b9caae3f0a4..ba75954ce71 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Questo account SimpliSafe \u00e8 gi\u00e0 in uso.", "email_2fa_timed_out": "Timeout durante l'attesa dell'autenticazione a due fattori basata su email.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "wrong_account": "Le credenziali utente fornite non corrispondono a questo account SimpliSafe." }, "error": { + "identifier_exists": "Account gi\u00e0 registrato", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Codice di autorizzazione", "password": "Password", "username": "Nome utente" }, - "description": "Digita il tuo nome utente e password." + "description": "SimpliSafe autentica gli utenti tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati, prima di iniziare, di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\nQuando sei pronto, fai clic [qui]({url}) per aprire l'app Web SimpliSafe e inserire le tue credenziali. Al termine del processo, ritorna qui e inserisci il codice di autorizzazione dall'URL dell'app Web SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index ccfb13b6cc1..74b30d2a9ef 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "A conta j\u00e1 foi configurada", "email_2fa_timed_out": "Expirou enquanto aguardava a autentica\u00e7\u00e3o de dois fatores enviada por e-mail.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "wrong_account": "As credenciais de usu\u00e1rio fornecidas n\u00e3o correspondem a esta conta SimpliSafe." }, "error": { + "identifier_exists": "Conta j\u00e1 cadastrada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "C\u00f3digo de autoriza\u00e7\u00e3o", "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "Insira seu nome de usu\u00e1rio e senha." + "description": "O SimpliSafe autentica os usu\u00e1rios por meio de seu aplicativo da web. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de come\u00e7ar. \n\n Quando estiver pronto, clique [aqui]( {url} ) para abrir o aplicativo Web SimpliSafe e insira suas credenciais. Quando o processo estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o da URL do aplicativo Web SimpliSafe." } } }, diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index 439524c8aa6..c1ab660e6a5 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "address": "Ger\u00e4teadresse", "mac": "MAC-Adresse des Ger\u00e4ts", "name": "Name", "password": "Passwort" diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 0407f71ece3..b583c60061b 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,12 +7,12 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, - "error": {}, "flow_title": "{name} ({address})", "step": { "user": { "data": { "address": "Device address", + "mac": "Device MAC address", "name": "Name", "password": "Password" }, @@ -24,7 +24,10 @@ "step": { "init": { "data": { - "retry_count": "Retry count" + "retry_count": "Retry count", + "retry_timeout": "Timeout between retries", + "scan_timeout": "How long to scan for advertisement data", + "update_time": "Time between updates (seconds)" } } } diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index 75eff0a9b7c..ea070f9f3c2 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Type Switchbot non pris en charge.", "unknown": "Erreur inattendue" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Adresse de l'appareil", "mac": "Adresse MAC de l'appareil", "name": "Nom", "password": "Mot de passe" diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index b8997f9247b..bcd6465acae 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -11,10 +11,11 @@ "one": "Vuoto", "other": "Vuoti" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Indirizzo del dispositivo", "mac": "Indirizzo MAC del dispositivo", "name": "Nome", "password": "Password" diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 3959425cbd3..3edc04ba6f3 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Tipo de Switchbot sem suporte.", "unknown": "Erro inesperado" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Endere\u00e7o do dispositivo", "mac": "Endere\u00e7o MAC do dispositivo", "name": "Nome", "password": "Senha" diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json index 36ea2123bdb..42e9f0b3e23 100644 --- a/homeassistant/components/twentemilieu/translations/de.json +++ b/homeassistant/components/twentemilieu/translations/de.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "house_letter": "Hausbrief/zusatz", + "house_letter": "Hausbrief/Zusatz", "house_number": "Hausnummer", "post_code": "Postleitzahl" }, diff --git a/homeassistant/components/uscis/translations/it.json b/homeassistant/components/uscis/translations/it.json index 1e23b69eee5..1cb9e54a6b7 100644 --- a/homeassistant/components/uscis/translations/it.json +++ b/homeassistant/components/uscis/translations/it.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "L'integrazione U.S. Citizenship and Immigration Services (USCIS) \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\n L'integrazione verr\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "L'integrazione U.S. Citizenship and Immigration Services (USCIS) \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione sar\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione USCIS verr\u00e0 rimossa" } } diff --git a/homeassistant/components/verisure/translations/it.json b/homeassistant/components/verisure/translations/it.json index b913a47af03..8361765c5c2 100644 --- a/homeassistant/components/verisure/translations/it.json +++ b/homeassistant/components/verisure/translations/it.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autenticazione non valida", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "unknown_mfa": "Si \u00e8 verificato un errore sconosciuto durante la configurazione dell'autenticazione a pi\u00f9 fattori" }, "step": { "installation": { @@ -30,7 +31,8 @@ }, "reauth_mfa": { "data": { - "code": "Codice di verifica" + "code": "Codice di verifica", + "description": "Il tuo account ha la verifica in due passaggi abilitata. Inserisci il codice di verifica che ti viene inviato da Verisure." } }, "user": { diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 412b75fc1b5..30836be5da5 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -29,7 +29,8 @@ "title": "Autentica nuovamente l'integrazione" }, "reauth_confirm": { - "description": "Il profilo \" {profile} \" deve essere riautenticato per poter continuare a ricevere i dati Withings." + "description": "Il profilo \"{profile}\" deve essere nuovamente autenticato per continuare a ricevere i dati di Withings.", + "title": "Autentica nuovamente l'integrazione" } } } diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index 81dda510bc5..b81386a076f 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "decryption_failed": "Der bereitgestellte Bindkey funktionierte nicht, Sensordaten konnten nicht entschl\u00fcsselt werden. Bitte \u00fcberpr\u00fcfe es und versuche es erneut.", + "expected_24_characters": "Erwartet wird ein 24-stelliger hexadezimaler Bindkey.", + "expected_32_characters": "Erwartet wird ein 32-stelliger hexadezimaler Bindkey.", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" }, "flow_title": "{name}", @@ -10,6 +13,12 @@ "bluetooth_confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "get_encryption_key_4_5": { + "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 32-stelligen hexadezimalen Bindungsschl\u00fcssel." + }, + "get_encryption_key_legacy": { + "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindungsschl\u00fcssel." + }, "user": { "data": { "address": "Ger\u00e4t" diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index d24df64f135..b9f4e024f92 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 32 character hexadecimal bindkey." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." + }, "user": { "data": { "address": "Device" diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json new file mode 100644 index 00000000000..749f7e45de5 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json index 8b24900933b..07426a0e290 100644 --- a/homeassistant/components/xiaomi_ble/translations/id.json +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -1,7 +1,21 @@ { "config": { "abort": { - "already_in_progress": "Alur konfigurasi sedang berlangsung" + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/it.json b/homeassistant/components/xiaomi_ble/translations/it.json new file mode 100644 index 00000000000..99adacee466 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/it.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controllare e riprovare.", + "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", + "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri.", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Chiave di collegamento" + }, + "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 32 caratteri." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Chiave di collegamento" + }, + "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 24 caratteri." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ja.json b/homeassistant/components/xiaomi_ble/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json new file mode 100644 index 00000000000..21c251bf0eb --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", + "expected_24_characters": "Espera-se uma bindkey hexadecimal de 24 caracteres.", + "expected_32_characters": "Esperado um bindkey hexadecimal de 32 caracteres.", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma bindkey hexadecimal de 24 caracteres." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 16ab4a84b6d..567ee5f359c 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Arvesta, et v\u00f5rgutoitega seadmed pole p\u00e4rast (sekundit) saadaval", "default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)", "enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga", + "enhanced_light_transition": "Luba t\u00e4iustatud valguse v\u00e4rvi/temperatuuri \u00fcleminek v\u00e4ljal\u00fclitatud olekust", "title": "\u00dcldised valikud" } }, diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 57be4c7acb6..30c85de48da 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Considera i dispositivi alimentati dalla rete non disponibili dopo (secondi)", "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", "enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete", + "enhanced_light_transition": "Abilita una transizione migliorata del colore/temperatura della luce da uno stato spento", "title": "Opzioni globali" } }, diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 9e8289960f8..14ba0b9280f 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "(\u79d2)\u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", "default_light_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e9\u30a4\u30c8\u9077\u79fb\u6642\u9593(\u79d2)", "enable_identify_on_join": "\u30c7\u30d0\u30a4\u30b9\u304c\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u53c2\u52a0\u3059\u308b\u969b\u306b\u3001\u8b58\u5225\u52b9\u679c\u3092\u6709\u52b9\u306b\u3059\u308b", + "enhanced_light_transition": "\u30aa\u30d5\u72b6\u614b\u304b\u3089\u3001\u30a8\u30f3\u30cf\u30f3\u30b9\u30c9\u30e9\u30a4\u30c8\u30ab\u30e9\u30fc/\u8272\u6e29\u5ea6\u3078\u306e\u9077\u79fb\u3092\u6709\u52b9\u306b\u3057\u307e\u3059", "title": "\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d7\u30b7\u30e7\u30f3" } }, From f31f2cca07135d7b20452cfc5db59f92dc4fa9a2 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Mon, 25 Jul 2022 11:03:56 +0100 Subject: [PATCH 675/820] Use DataUpdateCoordinator in london_underground (#75304) * Use DataUpdateCoordinator in london_underground * Update homeassistant/components/london_underground/sensor.py Co-authored-by: avee87 <6134677+avee87@users.noreply.github.com> * Follow up on PR comments * Removes unused callback import * Update homeassistant/components/london_underground/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/london_underground/sensor.py Co-authored-by: Martin Hjelmare * Adds missing PlatformNotReady import * Linting fixes Co-authored-by: avee87 <6134677+avee87@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- .../components/london_underground/sensor.py | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index a4cc66a8447..96ff9bc5056 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -2,17 +2,28 @@ from __future__ import annotations from datetime import timedelta +import logging +import async_timeout from london_tube_status import TubeData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "london_underground" ATTRIBUTION = "Powered by TfL Open Data" @@ -55,24 +66,46 @@ async def async_setup_platform( session = async_get_clientsession(hass) data = TubeData(session) - await data.update() + coordinator = LondonTubeCoordinator(hass, data) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise PlatformNotReady sensors = [] for line in config[CONF_LINE]: - sensors.append(LondonTubeSensor(line, data)) + sensors.append(LondonTubeSensor(coordinator, line)) - async_add_entities(sensors, True) + async_add_entities(sensors) -class LondonTubeSensor(SensorEntity): +class LondonTubeCoordinator(DataUpdateCoordinator): + """London Underground sensor coordinator.""" + + def __init__(self, hass, data): + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + self._data = data + + async def _async_update_data(self): + async with async_timeout.timeout(10): + await self._data.update() + return self._data.data + + +class LondonTubeSensor(CoordinatorEntity[LondonTubeCoordinator], SensorEntity): """Sensor that reads the status of a line from Tube Data.""" - def __init__(self, name, data): + def __init__(self, coordinator, name): """Initialize the London Underground sensor.""" - self._data = data - self._description = None + super().__init__(coordinator) self._name = name - self._state = None self.attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} @property @@ -83,7 +116,7 @@ class LondonTubeSensor(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return self._state + return self.coordinator.data[self.name]["State"] @property def icon(self): @@ -93,11 +126,5 @@ class LondonTubeSensor(SensorEntity): @property def extra_state_attributes(self): """Return other details about the sensor state.""" - self.attrs["Description"] = self._description + self.attrs["Description"] = self.coordinator.data[self.name]["Description"] return self.attrs - - async def async_update(self): - """Update the sensor.""" - await self._data.update() - self._state = self._data.data[self.name]["State"] - self._description = self._data.data[self.name]["Description"] From fc9a0ba46bbbf8f7d191569c5a81d74c9dd732ff Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 25 Jul 2022 20:20:15 +1000 Subject: [PATCH 676/820] Refactor Advantage Air classes for expansion (#75422) --- .../components/advantage_air/binary_sensor.py | 20 ++++-------- .../components/advantage_air/climate.py | 18 +++++------ .../components/advantage_air/cover.py | 7 ++-- .../components/advantage_air/entity.py | 32 +++++++++++++++---- .../components/advantage_air/select.py | 8 ++--- .../components/advantage_air/sensor.py | 26 ++++++--------- .../components/advantage_air/switch.py | 8 ++--- 7 files changed, 57 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 9fc53d7e1dc..c0934239fe7 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity PARALLEL_UPDATES = 0 @@ -38,7 +38,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): +class AdvantageAirFilter(AdvantageAirAcEntity, BinarySensorEntity): """Advantage Air Filter sensor.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM @@ -48,9 +48,7 @@ class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air Filter sensor.""" super().__init__(instance, ac_key) - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-filter' - ) + self._attr_unique_id += "-filter" @property def is_on(self): @@ -58,7 +56,7 @@ class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): return self._ac["filterCleanStatus"] -class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): +class AdvantageAirZoneMotion(AdvantageAirZoneEntity, BinarySensorEntity): """Advantage Air Zone Motion sensor.""" _attr_device_class = BinarySensorDeviceClass.MOTION @@ -67,9 +65,7 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): """Initialize an Advantage Air Zone Motion sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} motion' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-motion' - ) + self._attr_unique_id += "-motion" @property def is_on(self): @@ -77,7 +73,7 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): return self._zone["motion"] == 20 -class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): +class AdvantageAirZoneMyZone(AdvantageAirZoneEntity, BinarySensorEntity): """Advantage Air Zone MyZone sensor.""" _attr_entity_registry_enabled_default = False @@ -87,9 +83,7 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): """Initialize an Advantage Air Zone MyZone sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} myZone' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-myzone' - ) + self._attr_unique_id += "-myzone" @property def is_on(self): diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 1d89c313579..db060e739b1 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -25,7 +25,7 @@ from .const import ( ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN, ) -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity ADVANTAGE_AIR_HVAC_MODES = { "heat": HVACMode.HEAT, @@ -87,18 +87,13 @@ async def async_setup_entry( ) -class AdvantageAirClimateEntity(AdvantageAirEntity, ClimateEntity): - """AdvantageAir Climate class.""" +class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): + """AdvantageAir AC unit.""" _attr_temperature_unit = TEMP_CELSIUS _attr_target_temperature_step = PRECISION_WHOLE _attr_max_temp = 32 _attr_min_temp = 16 - - -class AdvantageAirAC(AdvantageAirClimateEntity): - """AdvantageAir AC unit.""" - _attr_fan_modes = [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] _attr_hvac_modes = AC_HVAC_MODES _attr_supported_features = ( @@ -108,7 +103,6 @@ class AdvantageAirAC(AdvantageAirClimateEntity): def __init__(self, instance, ac_key): """Initialize an AdvantageAir AC unit.""" super().__init__(instance, ac_key) - self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{ac_key}' if self._ac.get("myAutoModeEnabled"): self._attr_hvac_modes = AC_HVAC_MODES + [HVACMode.AUTO] @@ -159,9 +153,13 @@ class AdvantageAirAC(AdvantageAirClimateEntity): await self.async_change({self.ac_key: {"info": {"setTemp": temp}}}) -class AdvantageAirZone(AdvantageAirClimateEntity): +class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): """AdvantageAir Zone control.""" + _attr_temperature_unit = TEMP_CELSIUS + _attr_target_temperature_step = PRECISION_WHOLE + _attr_max_temp = 32 + _attr_min_temp = 16 _attr_hvac_modes = ZONE_HVAC_MODES _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 391f39953d2..4b3f371f52e 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -16,7 +16,7 @@ from .const import ( ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN, ) -from .entity import AdvantageAirEntity +from .entity import AdvantageAirZoneEntity PARALLEL_UPDATES = 0 @@ -39,7 +39,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): +class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): """Advantage Air Zone Vent.""" _attr_device_class = CoverDeviceClass.DAMPER @@ -53,9 +53,6 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): """Initialize an Advantage Air Zone Vent.""" super().__init__(instance, ac_key, zone_key) self._attr_name = self._zone["name"] - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}' - ) @property def is_closed(self) -> bool: diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 6c434518656..375bfa255c4 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -11,26 +11,44 @@ class AdvantageAirEntity(CoordinatorEntity): _attr_has_entity_name = True - def __init__(self, instance, ac_key, zone_key=None): - """Initialize common aspects of an Advantage Air sensor.""" + def __init__(self, instance): + """Initialize common aspects of an Advantage Air entity.""" super().__init__(instance["coordinator"]) + self._attr_unique_id = self.coordinator.data["system"]["rid"] + + +class AdvantageAirAcEntity(AdvantageAirEntity): + """Parent class for Advantage Air AC Entities.""" + + def __init__(self, instance, ac_key): + """Initialize common aspects of an Advantage Air ac entity.""" + super().__init__(instance) self.async_change = instance["async_change"] self.ac_key = ac_key - self.zone_key = zone_key + self._attr_unique_id += f"-{ac_key}" + self._attr_device_info = DeviceInfo( via_device=(DOMAIN, self.coordinator.data["system"]["rid"]), - identifiers={ - (DOMAIN, f"{self.coordinator.data['system']['rid']}_{ac_key}") - }, + identifiers={(DOMAIN, self._attr_unique_id)}, manufacturer="Advantage Air", model=self.coordinator.data["system"]["sysType"], - name=self._ac["name"], + name=self.coordinator.data["aircons"][self.ac_key]["info"]["name"], ) @property def _ac(self): return self.coordinator.data["aircons"][self.ac_key]["info"] + +class AdvantageAirZoneEntity(AdvantageAirAcEntity): + """Parent class for Advantage Air Zone Entities.""" + + def __init__(self, instance, ac_key, zone_key): + """Initialize common aspects of an Advantage Air zone entity.""" + super().__init__(instance, ac_key) + self.zone_key = zone_key + self._attr_unique_id += f"-{zone_key}" + @property def _zone(self): return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index 5dfa92c10ad..9cfece25b24 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity ADVANTAGE_AIR_INACTIVE = "Inactive" @@ -25,7 +25,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): +class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): """Representation of Advantage Air MyZone control.""" _attr_icon = "mdi:home-thermometer" @@ -37,9 +37,7 @@ class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-myzone' - ) + self._attr_unique_id += "-myzone" for zone in instance["coordinator"].data["aircons"][ac_key]["zones"].values(): if zone["type"] > 0: diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 370aab7b292..b110294b2fd 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity ADVANTAGE_AIR_SET_COUNTDOWN_VALUE = "minutes" ADVANTAGE_AIR_SET_COUNTDOWN_UNIT = "min" @@ -56,7 +56,7 @@ async def async_setup_entry( ) -class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): +class AdvantageAirTimeTo(AdvantageAirAcEntity, SensorEntity): """Representation of Advantage Air timer control.""" _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT @@ -68,9 +68,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): self.action = action self._time_key = f"countDownTo{action}" self._attr_name = f"Time to {action}" - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}' - ) + self._attr_unique_id += f"-timeto{action}" @property def native_value(self): @@ -90,7 +88,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): await self.async_change({self.ac_key: {"info": {self._time_key: value}}}) -class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): +class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity): """Representation of Advantage Air Zone Vent Sensor.""" _attr_native_unit_of_measurement = PERCENTAGE @@ -101,9 +99,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): """Initialize an Advantage Air Zone Vent Sensor.""" super().__init__(instance, ac_key, zone_key=zone_key) self._attr_name = f'{self._zone["name"]} vent' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-vent' - ) + self._attr_unique_id += "-vent" @property def native_value(self): @@ -120,7 +116,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): return "mdi:fan-off" -class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): +class AdvantageAirZoneSignal(AdvantageAirZoneEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" _attr_native_unit_of_measurement = PERCENTAGE @@ -131,9 +127,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): """Initialize an Advantage Air Zone wireless signal sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} signal' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal' - ) + self._attr_unique_id += "-signal" @property def native_value(self): @@ -154,7 +148,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): return "mdi:wifi-strength-outline" -class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): +class AdvantageAirZoneTemp(AdvantageAirZoneEntity, SensorEntity): """Representation of Advantage Air Zone temperature sensor.""" _attr_native_unit_of_measurement = TEMP_CELSIUS @@ -167,9 +161,7 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): """Initialize an Advantage Air Zone Temp Sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} temperature' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-temp' - ) + self._attr_unique_id += "-temp" @property def native_value(self): diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 504578c72e2..d9d46427599 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -9,7 +9,7 @@ from .const import ( ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN, ) -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity async def async_setup_entry( @@ -28,7 +28,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirFreshAir(AdvantageAirEntity, SwitchEntity): +class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity): """Representation of Advantage Air fresh air control.""" _attr_icon = "mdi:air-filter" @@ -37,9 +37,7 @@ class AdvantageAirFreshAir(AdvantageAirEntity, SwitchEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air fresh air control.""" super().__init__(instance, ac_key) - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-freshair' - ) + self._attr_unique_id += "-freshair" @property def is_on(self): From de46243ce5fd90b392e8f8511eecef159ee42c97 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Jul 2022 12:31:53 +0200 Subject: [PATCH 677/820] Raise YAML deprecation issue for Radio Therm (#75513) --- homeassistant/components/radiotherm/climate.py | 12 +++++++++++- homeassistant/components/radiotherm/manifest.json | 1 + homeassistant/components/radiotherm/strings.json | 6 ++++++ .../components/radiotherm/translations/en.json | 6 ++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index 3f8e87e74a4..c466a7108e8 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -18,6 +18,7 @@ from homeassistant.components.climate.const import ( HVACAction, HVACMode, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, @@ -125,13 +126,22 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Radio Thermostat.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( - # config flow added in 2022.7 and should be removed in 2022.9 "Configuration of the Radio Thermostat climate platform in YAML is deprecated and " "will be removed in Home Assistant 2022.9; Your existing configuration " "has been imported into the UI automatically and can be safely removed " "from your configuration.yaml file" ) + hosts: list[str] = [] if CONF_HOST in config: hosts = config[CONF_HOST] diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index c6ae4e5bb06..5c37b4e3cde 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -3,6 +3,7 @@ "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": ["radiotherm==2.1.0"], + "dependencies": ["repairs"], "codeowners": ["@bdraco", "@vinnyfuria"], "iot_class": "local_polling", "loggers": ["radiotherm"], diff --git a/homeassistant/components/radiotherm/strings.json b/homeassistant/components/radiotherm/strings.json index 22f17224285..51505d4d727 100644 --- a/homeassistant/components/radiotherm/strings.json +++ b/homeassistant/components/radiotherm/strings.json @@ -19,6 +19,12 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "issues": { + "deprecated_yaml": { + "title": "The Radio Thermostat YAML configuration is being removed", + "description": "Configuring the Radio Thermostat climate platform using YAML is being removed in Home Assistant 2022.9.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/en.json b/homeassistant/components/radiotherm/translations/en.json index b524f188e59..224c8ffeb3c 100644 --- a/homeassistant/components/radiotherm/translations/en.json +++ b/homeassistant/components/radiotherm/translations/en.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Radio Thermostat climate platform using YAML is being removed in Home Assistant 2022.9.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Radio Thermostat YAML configuration is being removed" + } + }, "options": { "step": { "init": { From b8ae883f1813240ec58f0b3e9a2e03d22af0abdd Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 25 Jul 2022 14:13:01 +0200 Subject: [PATCH 678/820] Set min transition time for Sengled lights in ZHA groups (#75644) * Set min transition time for Sengled lights in ZHA groups * Change test to expect correct min transition time for group with Sengled light * Fix turn_off with transition 0 for Sengled lights --- homeassistant/components/zha/light.py | 13 +++++++++---- tests/components/zha/test_light.py | 10 +++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6c26e17188b..190e0b2319d 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -74,6 +74,7 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" +DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} SUPPORT_GROUP_LIGHT = ( @@ -379,7 +380,7 @@ class BaseLight(LogMixin, light.LightEntity): # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( - 0, transition * 10 + 0, transition * 10 or self._DEFAULT_MIN_TRANSITION_TIME ) else: result = await self._on_off_channel.off() @@ -670,10 +671,10 @@ class ForceOnLight(Light): @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, - manufacturers={"Sengled"}, + manufacturers=DEFAULT_MIN_TRANSITION_MANUFACTURERS, ) -class SengledLight(Light): - """Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition.""" +class MinTransitionLight(Light): + """Representation of a light which does not react to any "move to" calls with 0 as a transition.""" _DEFAULT_MIN_TRANSITION_TIME = 1 @@ -688,6 +689,10 @@ class LightGroup(BaseLight, ZhaGroupEntity): """Initialize a light group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) group = self.zha_device.gateway.get_group(self._group_id) + self._DEFAULT_MIN_TRANSITION_TIME = any( # pylint: disable=invalid-name + member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS + for member in group.members + ) self._on_off_channel = group.endpoint[OnOff.cluster_id] self._level_channel = group.endpoint[LevelControl.cluster_id] self._color_channel = group.endpoint[Color.cluster_id] diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index c5b02f1a02f..94f0c96c38d 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1184,7 +1184,7 @@ async def async_test_off_from_hass(hass, cluster, entity_id): async def async_test_level_on_off_from_hass( - hass, on_off_cluster, level_cluster, entity_id + hass, on_off_cluster, level_cluster, entity_id, expected_default_transition: int = 0 ): """Test on off functionality from hass.""" @@ -1259,7 +1259,7 @@ async def async_test_level_on_off_from_hass( 4, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 10, - 0, + expected_default_transition, expect_reply=True, manufacturer=None, tries=1, @@ -1417,7 +1417,11 @@ async def test_zha_group_light_entity( # test turning the lights on and off from the HA await async_test_level_on_off_from_hass( - hass, group_cluster_on_off, group_cluster_level, group_entity_id + hass, + group_cluster_on_off, + group_cluster_level, + group_entity_id, + expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition ) # test getting a brightness change from the network From a813cf987bf11bd9c0ee6aea04d2299f39b26e07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 Jul 2022 09:52:35 -0500 Subject: [PATCH 679/820] Add bluetooth options flow to pick the adapter (#75701) --- .../components/bluetooth/__init__.py | 53 +++++--- .../components/bluetooth/config_flow.py | 44 ++++++- homeassistant/components/bluetooth/const.py | 7 ++ .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/match.py | 4 + homeassistant/components/bluetooth/models.py | 6 + .../components/bluetooth/strings.json | 12 +- .../components/bluetooth/translations/en.json | 12 +- homeassistant/components/bluetooth/util.py | 27 ++++ homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/bluetooth/test_config_flow.py | 115 +++++++++++++++++- tests/components/bluetooth/test_init.py | 59 ++++++++- tests/conftest.py | 2 +- 15 files changed, 318 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/bluetooth/util.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 9266603839c..551e93d5bd9 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -import platform from typing import Final, Union from bleak import BleakError @@ -29,7 +28,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_bluetooth from . import models -from .const import DOMAIN +from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN from .match import ( ADDRESS, BluetoothCallbackMatcher, @@ -38,6 +37,7 @@ from .match import ( ) from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher +from .util import async_get_bluetooth_adapters _LOGGER = logging.getLogger(__name__) @@ -175,15 +175,7 @@ def async_track_unavailable( async def _async_has_bluetooth_adapter() -> bool: """Return if the device has a bluetooth adapter.""" - if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware - return True - if platform.system() == "Windows": # We don't have a good way to detect on windows - return False - from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel - get_bluetooth_adapters, - ) - - return bool(await get_bluetooth_adapters()) + return bool(await async_get_bluetooth_adapters()) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -219,10 +211,22 @@ async def async_setup_entry( ) -> bool: """Set up the bluetooth integration from a config entry.""" manager: BluetoothManager = hass.data[DOMAIN] - await manager.async_start(BluetoothScanningMode.ACTIVE) + await manager.async_start( + BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) + ) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True +async def _async_update_listener( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> None: + """Handle options update.""" + manager: BluetoothManager = hass.data[DOMAIN] + manager.async_start_reload() + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: @@ -250,6 +254,7 @@ class BluetoothManager: self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] + self._reloading = False @hass_callback def async_setup(self) -> None: @@ -261,13 +266,29 @@ class BluetoothManager: """Get the scanner.""" return HaBleakScannerWrapper() - async def async_start(self, scanning_mode: BluetoothScanningMode) -> None: + @hass_callback + def async_start_reload(self) -> None: + """Start reloading.""" + self._reloading = True + + async def async_start( + self, scanning_mode: BluetoothScanningMode, adapter: str | None + ) -> None: """Set up BT Discovery.""" assert self.scanner is not None + if self._reloading: + # On reload, we need to reset the scanner instance + # since the devices in its history may not be reachable + # anymore. + self.scanner.async_reset() + self._integration_matcher.async_clear_history() + self._reloading = False + scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} + if adapter and adapter not in DEFAULT_ADAPTERS: + scanner_kwargs["adapter"] = adapter + _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) try: - self.scanner.async_setup( - scanning_mode=SCANNING_MODE_TO_BLEAK[scanning_mode] - ) + self.scanner.async_setup(**scanner_kwargs) except (FileNotFoundError, BleakError) as ex: raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex install_multiple_bleak_catcher() diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 8fe01be769d..bbba5f411b2 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -3,11 +3,15 @@ from __future__ import annotations from typing import Any +import voluptuous as vol + from homeassistant.components import onboarding -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .const import DEFAULT_NAME, DOMAIN +from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN +from .util import async_get_bluetooth_adapters class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): @@ -36,3 +40,39 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Handle import from configuration.yaml.""" return await self.async_step_enable_bluetooth(user_input) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(OptionsFlow): + """Handle the option flow for bluetooth.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + if not (adapters := await async_get_bluetooth_adapters()): + return self.async_abort(reason="no_adapters") + + data_schema = vol.Schema( + { + vol.Required( + CONF_ADAPTER, + default=self.config_entry.options.get(CONF_ADAPTER, adapters[0]), + ): vol.In(adapters), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 1e577f6064a..f3f00f581ee 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -2,3 +2,10 @@ DOMAIN = "bluetooth" DEFAULT_NAME = "Bluetooth" + +CONF_ADAPTER = "adapter" + +MACOS_DEFAULT_BLUETOOTH_ADAPTER = "CoreBluetooth" +UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" + +DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 551bb1c3733..c6ca8b11400 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.1"], + "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.2"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index c4560287feb..000f39eefd4 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -70,6 +70,10 @@ class IntegrationMatcher: MAX_REMEMBER_ADDRESSES ) + def async_clear_history(self) -> None: + """Clear the history.""" + self._matched = {} + def match_domains(self, device: BLEDevice, adv_data: AdvertisementData) -> set[str]: """Return the domains that are matched.""" matched_domains: set[str] = set() diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index e1d15c27243..408e0698879 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -67,6 +67,12 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] super().__init__(*args, **kwargs) self._setup = True + @hass_callback + def async_reset(self) -> None: + """Reset the scanner so it can be setup again.""" + self.history = {} + self._setup = False + @hass_callback def async_register_callback( self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index 328a001ad96..beff2fd8312 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -16,7 +16,17 @@ } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "no_adapters": "No Bluetooth adapters found" + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "The Bluetooth Adapter to use for scanning" + } + } } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 85019bdd689..4b53822b771 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "no_adapters": "No Bluetooth adapters found" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Choose a device to setup" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "The Bluetooth Adapter to use for scanning" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py new file mode 100644 index 00000000000..68920050748 --- /dev/null +++ b/homeassistant/components/bluetooth/util.py @@ -0,0 +1,27 @@ +"""The bluetooth integration utilities.""" +from __future__ import annotations + +import platform + +from .const import MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER + + +async def async_get_bluetooth_adapters() -> list[str]: + """Return a list of bluetooth adapters.""" + if platform.system() == "Windows": # We don't have a good way to detect on windows + return [] + if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware + return [MACOS_DEFAULT_BLUETOOTH_ADAPTER] + from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel + get_bluetooth_adapters, + ) + + adapters = await get_bluetooth_adapters() + if ( + UNIX_DEFAULT_BLUETOOTH_ADAPTER in adapters + and adapters[0] != UNIX_DEFAULT_BLUETOOTH_ADAPTER + ): + # The default adapter always needs to be the first in the list + # because that is how bleak works. + adapters.insert(0, adapters.pop(adapters.index(UNIX_DEFAULT_BLUETOOTH_ADAPTER))) + return adapters diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 568ba909ba5..3a6ae411c5e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.14.3 -bluetooth-adapters==0.1.1 +bluetooth-adapters==0.1.2 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3a39b405c91..64ccf9a0763 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -425,7 +425,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.1 +bluetooth-adapters==0.1.2 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9f699d61d8..03538ea10cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.1 +bluetooth-adapters==0.1.2 # homeassistant.components.bond bond-async==0.1.22 diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 5c6199b9bf0..1053133cac9 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -3,7 +3,11 @@ from unittest.mock import patch from homeassistant import config_entries -from homeassistant.components.bluetooth.const import DOMAIN +from homeassistant.components.bluetooth.const import ( + CONF_ADAPTER, + DOMAIN, + MACOS_DEFAULT_BLUETOOTH_ADAPTER, +) from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -125,3 +129,112 @@ async def test_async_step_import_already_exists(hass): ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Linux") +async def test_options_flow_linux(mock_system, hass, mock_bleak_scanner_start): + """Test options on Linux.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="DOMAIN", + ) + entry.add_to_hass(hass) + + # Verify we can keep it as hci0 + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADAPTER: "hci0", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_ADAPTER] == "hci0" + + # Verify we can change it to hci1 + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] + ): + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADAPTER: "hci1", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_ADAPTER] == "hci1" + + +@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Darwin") +async def test_options_flow_macos(mock_system, hass, mock_bleak_scanner_start): + """Test options on MacOS.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="DOMAIN", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADAPTER: MACOS_DEFAULT_BLUETOOTH_ADAPTER, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_ADAPTER] == MACOS_DEFAULT_BLUETOOTH_ADAPTER + + +@patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Windows" +) +async def test_options_flow_windows(mock_system, hass, mock_bleak_scanner_start): + """Test options on Windows.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="DOMAIN", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_adapters" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index aa3a6253b83..bb2c5f49cc9 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -15,6 +15,10 @@ from homeassistant.components.bluetooth import ( async_track_unavailable, models, ) +from homeassistant.components.bluetooth.const import ( + CONF_ADAPTER, + UNIX_DEFAULT_BLUETOOTH_ADAPTER, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -1160,9 +1164,22 @@ async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_blue async def test_auto_detect_bluetooth_adapters_linux(hass): """Test we auto detect bluetooth adapters on linux.""" with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value={"hci0"} + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0"] ), patch( - "homeassistant.components.bluetooth.platform.system", return_value="Linux" + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + + +async def test_auto_detect_bluetooth_adapters_linux_multiple(hass): + """Test we auto detect bluetooth adapters on linux with multiple adapters.""" + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci1", "hci0"] + ), patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -1173,7 +1190,7 @@ async def test_auto_detect_bluetooth_adapters_linux(hass): async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): """Test we auto detect bluetooth adapters on linux with no adapters found.""" with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()), patch( - "homeassistant.components.bluetooth.platform.system", return_value="Linux" + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -1184,7 +1201,7 @@ async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): async def test_auto_detect_bluetooth_adapters_macos(hass): """Test we auto detect bluetooth adapters on macos.""" with patch( - "homeassistant.components.bluetooth.platform.system", return_value="Darwin" + "homeassistant.components.bluetooth.util.platform.system", return_value="Darwin" ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -1195,7 +1212,8 @@ async def test_auto_detect_bluetooth_adapters_macos(hass): async def test_no_auto_detect_bluetooth_adapters_windows(hass): """Test we auto detect bluetooth adapters on windows.""" with patch( - "homeassistant.components.bluetooth.platform.system", return_value="Windows" + "homeassistant.components.bluetooth.util.platform.system", + return_value="Windows", ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -1230,3 +1248,34 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( assert entry.state == ConfigEntryState.LOADED assert "Error stopping scanner" in caplog.text + + +async def test_changing_the_adapter_at_runtime(hass): + """Test we can change the adapter at runtime.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, + data={}, + options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + ) as mock_setup, patch( + "homeassistant.components.bluetooth.HaBleakScanner.start" + ), patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop" + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "adapter" not in mock_setup.mock_calls[0][2] + + entry.options = {CONF_ADAPTER: "hci1"} + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + assert mock_setup.mock_calls[1][2]["adapter"] == "hci1" + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index e0f4fb5ab90..50c24df8d44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -885,7 +885,7 @@ async def mock_enable_bluetooth( @pytest.fixture(name="mock_bluetooth_adapters") def mock_bluetooth_adapters(): """Fixture to mock bluetooth adapters.""" - with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()): + with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=[]): yield From e3fb4ceb099bfe77375c5040f16eaa79ceacdcd8 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 25 Jul 2022 11:31:05 -0400 Subject: [PATCH 680/820] Bump AIOAladdinConnect to 0.1.31 (#75721) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 4a04bf69aed..d551b91bce9 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.27"], + "requirements": ["AIOAladdinConnect==0.1.31"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 64ccf9a0763..4cdebd91a2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.27 +AIOAladdinConnect==0.1.31 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03538ea10cc..83afdad8daa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.27 +AIOAladdinConnect==0.1.31 # homeassistant.components.adax Adax-local==0.1.4 From f4e7436421479d902569c919d8e7279812cd9571 Mon Sep 17 00:00:00 2001 From: hahn-th Date: Mon, 25 Jul 2022 18:15:02 +0200 Subject: [PATCH 681/820] Add device HmIP-STE2-PCB to homematicip_cloud (#75369) --- .../homematicip_cloud/manifest.json | 2 +- .../components/homematicip_cloud/sensor.py | 80 ++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_sensor.py | 75 +++++++++++++++ tests/fixtures/homematicip_cloud.json | 94 +++++++++++++++++++ 7 files changed, 253 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index db0833f8114..0d06d595f1b 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.5"], + "requirements": ["homematicip==1.0.7"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 80cdbd351b1..57a8b7bd714 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -17,6 +17,7 @@ from homematicip.aio.device import ( AsyncPlugableSwitchMeasuring, AsyncPresenceDetectorIndoor, AsyncRoomControlDeviceAnalog, + AsyncTemperatureDifferenceSensor2, AsyncTemperatureHumiditySensorDisplay, AsyncTemperatureHumiditySensorOutdoor, AsyncTemperatureHumiditySensorWithoutDisplay, @@ -124,6 +125,10 @@ async def async_setup_entry( entities.append(HomematicipTodayRainSensor(hap, device)) if isinstance(device, AsyncPassageDetector): entities.append(HomematicipPassageDetectorDeltaCounter(hap, device)) + if isinstance(device, AsyncTemperatureDifferenceSensor2): + entities.append(HomematicpTemperatureExternalSensorCh1(hap, device)) + entities.append(HomematicpTemperatureExternalSensorCh2(hap, device)) + entities.append(HomematicpTemperatureExternalSensorDelta(hap, device)) if entities: async_add_entities(entities) @@ -387,6 +392,81 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity): return LENGTH_MILLIMETERS +class HomematicpTemperatureExternalSensorCh1(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP device HmIP-STE2-PCB.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Channel 1 Temperature") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_value(self) -> float: + """Return the state.""" + return self._device.temperatureExternalOne + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + +class HomematicpTemperatureExternalSensorCh2(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP device HmIP-STE2-PCB.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Channel 2 Temperature") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_value(self) -> float: + """Return the state.""" + return self._device.temperatureExternalTwo + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + +class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP device HmIP-STE2-PCB.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Delta Temperature") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_value(self) -> float: + """Return the state.""" + return self._device.temperatureExternalDelta + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP passage detector delta counter.""" diff --git a/requirements_all.txt b/requirements_all.txt index 4cdebd91a2a..b56c176652a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -846,7 +846,7 @@ home-assistant-frontend==20220707.1 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.5 +homematicip==1.0.7 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83afdad8daa..8e5146bcfc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -622,7 +622,7 @@ home-assistant-frontend==20220707.1 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.5 +homematicip==1.0.7 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 8e3d80ca839..44b91c4ed47 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 258 + assert len(mock_hap.hmip_device_by_entity_id) == 262 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 34c119595b3..823508d5fee 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -340,6 +340,81 @@ async def test_hmip_today_rain_sensor(hass, default_mock_hap_factory): assert ha_state.state == "14.2" +async def test_hmip_temperature_external_sensor_channel_1( + hass, default_mock_hap_factory +): + """Test HomematicipTemperatureDifferenceSensor Channel 1 HmIP-STE2-PCB.""" + entity_id = "sensor.ste2_channel_1_temperature" + entity_name = "STE2 Channel 1 Temperature" + device_model = "HmIP-STE2-PCB" + + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["STE2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) + + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalOne", 25.4) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "25.4" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalOne", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + +async def test_hmip_temperature_external_sensor_channel_2( + hass, default_mock_hap_factory +): + """Test HomematicipTemperatureDifferenceSensor Channel 2 HmIP-STE2-PCB.""" + entity_id = "sensor.ste2_channel_2_temperature" + entity_name = "STE2 Channel 2 Temperature" + device_model = "HmIP-STE2-PCB" + + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["STE2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) + + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalTwo", 22.4) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "22.4" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalTwo", 23.4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.4" + + +async def test_hmip_temperature_external_sensor_delta(hass, default_mock_hap_factory): + """Test HomematicipTemperatureDifferenceSensor Delta HmIP-STE2-PCB.""" + entity_id = "sensor.ste2_delta_temperature" + entity_name = "STE2 Delta Temperature" + device_model = "HmIP-STE2-PCB" + + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["STE2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) + + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalDelta", 0.4) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "0.4" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data( + hass, hmip_device, "temperatureExternalDelta", -0.5 + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "-0.5" + + async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap_factory): """Test HomematicipPassageDetectorDeltaCounter.""" entity_id = "sensor.spdr_1" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 2c125d9fada..b0037aa3800 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -6631,6 +6631,100 @@ "serializedGlobalTradeItemNumber": "3014F7110000000000056775", "type": "FULL_FLUSH_CONTACT_INTERFACE_6", "updateState": "UP_TO_DATE" + }, + "3014F7110000000000STE2015": { + "availableFirmwareVersion": "1.0.26", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.18", + "firmwareVersionInteger": 65554, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000STE2015", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000024"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -60, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000STE2015", + "functionalChannelType": "TEMPERATURE_SENSOR_2_EXTERNAL_DELTA_CHANNEL", + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000025"], + "index": 1, + "label": "", + "temperatureExternalDelta": -0.9, + "temperatureExternalOne": 24.5, + "temperatureExternalTwo": 25.4 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000STE2015", + "label": "STE2", + "lastStatusUpdate": 1645012379988, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 415, + "modelType": "HmIP-STE2-PCB", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000STE2015", + "type": "TEMPERATURE_SENSOR_2_EXTERNAL_DELTA", + "updateState": "TRANSFERING_UPDATE" } }, "groups": { From 2df20e7a42c23b65f11f13d51e19b6e75dfdab08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 Jul 2022 13:21:43 -0500 Subject: [PATCH 682/820] Make lifx async_migrate_legacy_entries a callback (#75719) --- homeassistant/components/lifx/__init__.py | 4 ++-- homeassistant/components/lifx/migration.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 8816226ff84..3faf69483d5 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -77,7 +77,7 @@ async def async_legacy_migration( } # device.mac_addr is not the mac_address, its the serial number hosts_by_serial = {device.mac_addr: device.ip_addr for device in discovered_devices} - missing_discovery_count = await async_migrate_legacy_entries( + missing_discovery_count = async_migrate_legacy_entries( hass, hosts_by_serial, existing_serials, legacy_entry ) if missing_discovery_count: @@ -180,7 +180,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if legacy_entry := async_get_legacy_entry(hass): # If the legacy entry still exists, harvest the entities # that are moving to this config entry. - await async_migrate_entities_devices(hass, legacy_entry.entry_id, entry) + async_migrate_entities_devices(hass, legacy_entry.entry_id, entry) assert entry.unique_id is not None domain_data = hass.data[DOMAIN] diff --git a/homeassistant/components/lifx/migration.py b/homeassistant/components/lifx/migration.py index 1ff94daa92f..359480a4507 100644 --- a/homeassistant/components/lifx/migration.py +++ b/homeassistant/components/lifx/migration.py @@ -2,14 +2,15 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import _LOGGER, DOMAIN from .discovery import async_init_discovery_flow -async def async_migrate_legacy_entries( +@callback +def async_migrate_legacy_entries( hass: HomeAssistant, discovered_hosts_by_serial: dict[str, str], existing_serials: set[str], @@ -41,7 +42,8 @@ async def async_migrate_legacy_entries( return len(remaining_devices) -async def async_migrate_entities_devices( +@callback +def async_migrate_entities_devices( hass: HomeAssistant, legacy_entry_id: str, new_entry: ConfigEntry ) -> None: """Move entities and devices to the new config entry.""" From 3aa75f3fccf47224ad59b141d39b55e7180e823a Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 26 Jul 2022 04:32:00 +0800 Subject: [PATCH 683/820] Don't use executor for lutron subscription (#75726) --- homeassistant/components/lutron/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index b2b01aa3c44..1583d8b74eb 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -124,9 +124,7 @@ class LutronDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.async_add_executor_job( - self._lutron_device.subscribe, self._update_callback, None - ) + self._lutron_device.subscribe(self._update_callback, None) def _update_callback(self, _device, _context, _event, _params): """Run when invoked by pylutron when the device state changes.""" From 274584f2a47b7db9943168316122196a540080e5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 25 Jul 2022 22:52:13 +0200 Subject: [PATCH 684/820] Add strict typing for litterrobot (#75540) --- .strict-typing | 1 + .../components/litterrobot/__init__.py | 1 + .../components/litterrobot/config_flow.py | 9 ++++++- .../components/litterrobot/entity.py | 25 +++++++++++++------ homeassistant/components/litterrobot/hub.py | 3 ++- .../components/litterrobot/sensor.py | 11 ++++---- .../components/litterrobot/switch.py | 14 ++++++----- mypy.ini | 11 ++++++++ 8 files changed, 53 insertions(+), 22 deletions(-) diff --git a/.strict-typing b/.strict-typing index e64a9a04931..45d11f089dd 100644 --- a/.strict-typing +++ b/.strict-typing @@ -152,6 +152,7 @@ homeassistant.components.laundrify.* homeassistant.components.lcn.* homeassistant.components.light.* homeassistant.components.lifx.* +homeassistant.components.litterrobot.* homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.logbook.* diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index a2612966a98..f3b150f2c1d 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,4 +1,5 @@ """The Litter-Robot integration.""" +from __future__ import annotations from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index c49d18c5257..fbe32fa9749 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -1,11 +1,16 @@ """Config flow for Litter-Robot integration.""" +from __future__ import annotations + +from collections.abc import Mapping import logging +from typing import Any from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN from .hub import LitterRobotHub @@ -22,7 +27,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 51b88bb4f79..501b71fbd06 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,29 +1,35 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations +from collections.abc import Callable, Coroutine from datetime import time import logging -from types import MethodType from typing import Any from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException +from typing_extensions import ParamSpec from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) import homeassistant.util.dt as dt_util from .const import DOMAIN from .hub import LitterRobotHub +_P = ParamSpec("_P") + _LOGGER = logging.getLogger(__name__) REFRESH_WAIT_TIME_SECONDS = 8 -class LitterRobotEntity(CoordinatorEntity): +class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): """Generic Litter-Robot entity representing common data and methods.""" def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: @@ -63,7 +69,10 @@ class LitterRobotControlEntity(LitterRobotEntity): self._refresh_callback: CALLBACK_TYPE | None = None async def perform_action_and_refresh( - self, action: MethodType, *args: Any, **kwargs: Any + self, + action: Callable[_P, Coroutine[Any, Any, bool]], + *args: _P.args, + **kwargs: _P.kwargs, ) -> bool: """Perform an action and initiates a refresh of the robot data after a few seconds.""" success = False @@ -82,7 +91,7 @@ class LitterRobotControlEntity(LitterRobotEntity): ) return success - async def async_call_later_callback(self, *_) -> None: + async def async_call_later_callback(self, *_: Any) -> None: """Perform refresh request on callback.""" self._refresh_callback = None await self.coordinator.async_request_refresh() @@ -92,7 +101,7 @@ class LitterRobotControlEntity(LitterRobotEntity): self.async_cancel_refresh_callback() @callback - def async_cancel_refresh_callback(self): + def async_cancel_refresh_callback(self) -> None: """Clear the refresh callback if it has not already fired.""" if self._refresh_callback is not None: self._refresh_callback() @@ -126,10 +135,10 @@ class LitterRobotConfigEntity(LitterRobotControlEntity): def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) - self._assumed_state: Any = None + self._assumed_state: bool | None = None async def perform_action_and_assume_state( - self, action: MethodType, assumed_state: Any + self, action: Callable[[bool], Coroutine[Any, Any, bool]], assumed_state: bool ) -> None: """Perform an action and assume the state passed in if call is successful.""" if await self.perform_action_and_refresh(action, assumed_state): diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 43d60e534ea..bde4c780482 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any from pylitterbot import Account from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException @@ -24,7 +25,7 @@ class LitterRobotHub: account: Account - def __init__(self, hass: HomeAssistant, data: Mapping) -> None: + def __init__(self, hass: HomeAssistant, data: Mapping[str, Any]) -> None: """Initialize the Litter-Robot hub.""" self._data = data diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 01deaa302cf..b6dd2a976c3 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -from typing import Any +from typing import Any, Union, cast from pylitterbot.robot import Robot @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, - StateType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE @@ -61,12 +60,12 @@ class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): self.entity_description = description @property - def native_value(self) -> StateType | datetime: + def native_value(self) -> float | datetime | str | None: """Return the state.""" if self.entity_description.should_report(self.robot): if isinstance(val := getattr(self.robot, self.entity_description.key), str): return val.lower() - return val + return cast(Union[float, datetime, None], val) return None @property @@ -88,13 +87,13 @@ ROBOT_SENSORS = [ name="Sleep Mode Start Time", key="sleep_mode_start_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, + should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] ), LitterRobotSensorEntityDescription( name="Sleep Mode End Time", key="sleep_mode_end_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, + should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] ), LitterRobotSensorEntityDescription( name="Last Seen", diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 4d302a0d4ae..5374add1e34 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -17,11 +17,11 @@ class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): """Litter-Robot Night Light Mode Switch.""" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.night_light_mode_enabled + return self.robot.night_light_mode_enabled # type: ignore[no-any-return] @property def icon(self) -> str: @@ -41,11 +41,11 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): """Litter-Robot Panel Lockout Switch.""" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.panel_lock_enabled + return self.robot.panel_lock_enabled # type: ignore[no-any-return] @property def icon(self) -> str: @@ -61,7 +61,9 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): await self.perform_action_and_assume_state(self.robot.set_panel_lockout, False) -ROBOT_SWITCHES: list[tuple[type[LitterRobotConfigEntity], str]] = [ +ROBOT_SWITCHES: list[ + tuple[type[LitterRobotNightLightModeSwitch | LitterRobotPanelLockoutSwitch], str] +] = [ (LitterRobotNightLightModeSwitch, "Night Light Mode"), (LitterRobotPanelLockoutSwitch, "Panel Lockout"), ] @@ -75,7 +77,7 @@ async def async_setup_entry( """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities = [] + entities: list[SwitchEntity] = [] for robot in hub.account.robots: for switch_class, switch_type in ROBOT_SWITCHES: entities.append(switch_class(robot=robot, entity_type=switch_type, hub=hub)) diff --git a/mypy.ini b/mypy.ini index ee953f13d74..af6abe6658f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1395,6 +1395,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.litterrobot.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.local_ip.*] check_untyped_defs = true disallow_incomplete_defs = true From a287abe76377479be09f8d82d29e128de96a6f14 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 25 Jul 2022 15:53:22 -0500 Subject: [PATCH 685/820] Update name of Z-WaveJS to Z-Wave (#75136) --- homeassistant/components/zwave_js/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index d1097a6cd65..76f5d94b589 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -1,6 +1,6 @@ { "domain": "zwave_js", - "name": "Z-Wave JS", + "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", "requirements": ["zwave-js-server-python==0.39.0"], From ea354f3d5fb4ec04f740c0fb416bb0c355aa3eaa Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 26 Jul 2022 07:56:36 +1000 Subject: [PATCH 686/820] Remove deprecated MyZone service in Advantage Air (#75160) --- .../components/advantage_air/climate.py | 18 ------------------ .../components/advantage_air/services.yaml | 8 -------- tests/components/advantage_air/test_climate.py | 17 ----------------- 3 files changed, 43 deletions(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index db060e739b1..e69dc06dd7d 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -15,7 +15,6 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -53,7 +52,6 @@ ADVANTAGE_AIR_FAN_MODES = { HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()} FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100} -ADVANTAGE_AIR_SERVICE_SET_MYZONE = "set_myzone" ZONE_HVAC_MODES = [HVACMode.OFF, HVACMode.HEAT_COOL] PARALLEL_UPDATES = 0 @@ -79,13 +77,6 @@ async def async_setup_entry( entities.append(AdvantageAirZone(instance, ac_key, zone_key)) async_add_entities(entities) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - ADVANTAGE_AIR_SERVICE_SET_MYZONE, - {}, - "set_myzone", - ) - class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): """AdvantageAir AC unit.""" @@ -213,12 +204,3 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): await self.async_change( {self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}} ) - - async def set_myzone(self, **kwargs): - """Set this zone as the 'MyZone'.""" - _LOGGER.warning( - "The advantage_air.set_myzone service has been deprecated and will be removed in a future version, please use the select.select_option service on the MyZone entity" - ) - await self.async_change( - {self.ac_key: {"info": {"myZone": self._zone["number"]}}} - ) diff --git a/homeassistant/components/advantage_air/services.yaml b/homeassistant/components/advantage_air/services.yaml index 38f21e57128..6bd3bf815d6 100644 --- a/homeassistant/components/advantage_air/services.yaml +++ b/homeassistant/components/advantage_air/services.yaml @@ -15,11 +15,3 @@ set_time_to: min: 0 max: 1440 unit_of_measurement: minutes - -set_myzone: - name: Set MyZone - description: Change which zone is set as the reference for temperature control (deprecated) - target: - entity: - integration: advantage_air - domain: climate diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 6ee0a614b09..f1b118ab3b3 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -2,14 +2,12 @@ from json import loads from homeassistant.components.advantage_air.climate import ( - ADVANTAGE_AIR_SERVICE_SET_MYZONE, HASS_FAN_MODES, HASS_HVAC_MODES, ) from homeassistant.components.advantage_air.const import ( ADVANTAGE_AIR_STATE_OFF, ADVANTAGE_AIR_STATE_ON, - DOMAIN as ADVANTAGE_AIR_DOMAIN, ) from homeassistant.components.climate.const import ( ATTR_FAN_MODE, @@ -170,21 +168,6 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][0] == "GET" assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" - # Test set_myair service - await hass.services.async_call( - ADVANTAGE_AIR_DOMAIN, - ADVANTAGE_AIR_SERVICE_SET_MYZONE, - {ATTR_ENTITY_ID: [entity_id]}, - blocking=True, - ) - assert len(aioclient_mock.mock_calls) == 17 - assert aioclient_mock.mock_calls[-2][0] == "GET" - assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" - data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) - assert data["ac1"]["info"]["myZone"] == 1 - assert aioclient_mock.mock_calls[-1][0] == "GET" - assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" - async def test_climate_async_failed_update(hass, aioclient_mock): """Test climate change failure.""" From e87c2b9e2590eadcaa14f7256388675fcc64918d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 26 Jul 2022 00:45:01 +0200 Subject: [PATCH 687/820] Bump pytraccar to 1.0.0 (#75671) --- .../components/traccar/device_tracker.py | 182 +++++++++--------- .../components/traccar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/traccar/test_device_tracker.py | 43 +++-- 5 files changed, 128 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 50b0e7827aa..970cd20d640 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -1,11 +1,19 @@ """Support for Traccar device tracking.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging -from pytraccar.api import API +from pytraccar import ( + ApiClient, + DeviceModel, + GeofenceModel, + PositionModel, + TraccarAuthenticationException, + TraccarException, +) from stringcase import camelcase import voluptuous as vol @@ -170,17 +178,13 @@ async def async_setup_scanner( discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Validate the configuration and return a Traccar scanner.""" - - session = async_get_clientsession(hass, config[CONF_VERIFY_SSL]) - - api = API( - hass.loop, - session, - config[CONF_USERNAME], - config[CONF_PASSWORD], - config[CONF_HOST], - config[CONF_PORT], - config[CONF_SSL], + api = ApiClient( + host=config[CONF_HOST], + port=config[CONF_PORT], + ssl=config[CONF_SSL], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + client_session=async_get_clientsession(hass, config[CONF_VERIFY_SSL]), ) scanner = TraccarScanner( @@ -202,15 +206,15 @@ class TraccarScanner: def __init__( self, - api, - hass, - async_see, - scan_interval, - max_accuracy, - skip_accuracy_on, - custom_attributes, - event_types, - ): + api: ApiClient, + hass: HomeAssistant, + async_see: Callable[..., Awaitable[None]], + scan_interval: timedelta, + max_accuracy: int, + skip_accuracy_on: bool, + custom_attributes: list[str], + event_types: list[str], + ) -> None: """Initialize.""" if EVENT_ALL_EVENTS in event_types: @@ -220,15 +224,18 @@ class TraccarScanner: self._scan_interval = scan_interval self._async_see = async_see self._api = api - self.connected = False self._hass = hass self._max_accuracy = max_accuracy self._skip_accuracy_on = skip_accuracy_on + self._devices: list[DeviceModel] = [] + self._positions: list[PositionModel] = [] + self._geofences: list[GeofenceModel] = [] async def async_init(self): """Further initialize connection to Traccar.""" - await self._api.test_connection() - if self._api.connected and not self._api.authenticated: + try: + await self._api.get_server() + except TraccarAuthenticationException: _LOGGER.error("Authentication for Traccar failed") return False @@ -238,57 +245,63 @@ class TraccarScanner: async def _async_update(self, now=None): """Update info from Traccar.""" - if not self.connected: - _LOGGER.debug("Testing connection to Traccar") - await self._api.test_connection() - self.connected = self._api.connected - if self.connected: - _LOGGER.info("Connection to Traccar restored") - else: - return _LOGGER.debug("Updating device data") - await self._api.get_device_info(self._custom_attributes) + try: + (self._devices, self._positions, self._geofences,) = await asyncio.gather( + self._api.get_devices(), + self._api.get_positions(), + self._api.get_geofences(), + ) + except TraccarException as ex: + _LOGGER.error("Error while updating device data: %s", ex) + return + self._hass.async_create_task(self.import_device_data()) if self._event_types: self._hass.async_create_task(self.import_events()) - self.connected = self._api.connected async def import_device_data(self): """Import device data from Traccar.""" - for device_unique_id in self._api.device_info: - device_info = self._api.device_info[device_unique_id] - device = None - attr = {} + for position in self._positions: + device = next( + (dev for dev in self._devices if dev.id == position.device_id), None + ) + + if not device: + continue + + attr = { + ATTR_TRACKER: "traccar", + ATTR_ADDRESS: position.address, + ATTR_SPEED: position.speed, + ATTR_ALTITUDE: position.altitude, + ATTR_MOTION: position.attributes.get("motion", False), + ATTR_TRACCAR_ID: device.id, + ATTR_GEOFENCE: next( + ( + geofence.name + for geofence in self._geofences + if geofence.id in (device.geofence_ids or []) + ), + None, + ), + ATTR_CATEGORY: device.category, + ATTR_STATUS: device.status, + } + skip_accuracy_filter = False - attr[ATTR_TRACKER] = "traccar" - if device_info.get("address") is not None: - attr[ATTR_ADDRESS] = device_info["address"] - if device_info.get("geofence") is not None: - attr[ATTR_GEOFENCE] = device_info["geofence"] - if device_info.get("category") is not None: - attr[ATTR_CATEGORY] = device_info["category"] - if device_info.get("speed") is not None: - attr[ATTR_SPEED] = device_info["speed"] - if device_info.get("motion") is not None: - attr[ATTR_MOTION] = device_info["motion"] - if device_info.get("traccar_id") is not None: - attr[ATTR_TRACCAR_ID] = device_info["traccar_id"] - for dev in self._api.devices: - if dev["id"] == device_info["traccar_id"]: - device = dev - break - if device is not None and device.get("status") is not None: - attr[ATTR_STATUS] = device["status"] for custom_attr in self._custom_attributes: - if device_info.get(custom_attr) is not None: - attr[custom_attr] = device_info[custom_attr] + if device.attributes.get(custom_attr) is not None: + attr[custom_attr] = position.attributes[custom_attr] + if custom_attr in self._skip_accuracy_on: + skip_accuracy_filter = True + if position.attributes.get(custom_attr) is not None: + attr[custom_attr] = position.attributes[custom_attr] if custom_attr in self._skip_accuracy_on: skip_accuracy_filter = True - accuracy = 0.0 - if device_info.get("accuracy") is not None: - accuracy = device_info["accuracy"] + accuracy = position.accuracy or 0.0 if ( not skip_accuracy_filter and self._max_accuracy > 0 @@ -302,42 +315,39 @@ class TraccarScanner: continue await self._async_see( - dev_id=slugify(device_info["device_id"]), - gps=(device_info.get("latitude"), device_info.get("longitude")), + dev_id=slugify(device.name), + gps=(position.latitude, position.longitude), gps_accuracy=accuracy, - battery=device_info.get("battery"), + battery=position.attributes.get("batteryLevel", -1), attributes=attr, ) async def import_events(self): """Import events from Traccar.""" - device_ids = [device["id"] for device in self._api.devices] - end_interval = datetime.utcnow() - start_interval = end_interval - self._scan_interval - events = await self._api.get_events( - device_ids=device_ids, - from_time=start_interval, - to_time=end_interval, + start_intervel = datetime.utcnow() + events = await self._api.get_reports_events( + devices=[device.id for device in self._devices], + start_time=start_intervel, + end_time=start_intervel - self._scan_interval, event_types=self._event_types.keys(), ) if events is not None: for event in events: - device_name = next( - ( - dev.get("name") - for dev in self._api.devices - if dev.get("id") == event["deviceId"] - ), - None, - ) self._hass.bus.async_fire( - f"traccar_{self._event_types.get(event['type'])}", + f"traccar_{self._event_types.get(event.type)}", { - "device_traccar_id": event["deviceId"], - "device_name": device_name, - "type": event["type"], - "serverTime": event.get("eventTime") or event.get("serverTime"), - "attributes": event["attributes"], + "device_traccar_id": event.device_id, + "device_name": next( + ( + dev.name + for dev in self._devices + if dev.id == event.device_id + ), + None, + ), + "type": event.type, + "serverTime": event.event_time, + "attributes": event.attributes, }, ) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 7f0df1b1f3f..d7b26100ab6 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -3,7 +3,7 @@ "name": "Traccar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/traccar", - "requirements": ["pytraccar==0.10.0", "stringcase==1.2.0"], + "requirements": ["pytraccar==1.0.0", "stringcase==1.2.0"], "dependencies": ["webhook"], "codeowners": ["@ludeeus"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index b56c176652a..fa189bc1d0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pytomorrowio==0.3.4 pytouchline==0.7 # homeassistant.components.traccar -pytraccar==0.10.0 +pytraccar==1.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e5146bcfc2..9fca788e61e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1340,7 +1340,7 @@ pytile==2022.02.0 pytomorrowio==0.3.4 # homeassistant.components.traccar -pytraccar==0.10.0 +pytraccar==1.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.0 diff --git a/tests/components/traccar/test_device_tracker.py b/tests/components/traccar/test_device_tracker.py index 4e2f5e0ff09..61bbe371a75 100644 --- a/tests/components/traccar/test_device_tracker.py +++ b/tests/components/traccar/test_device_tracker.py @@ -2,6 +2,8 @@ from datetime import datetime from unittest.mock import AsyncMock, patch +from pytraccar import ReportsEventeModel + from homeassistant.components.device_tracker.const import DOMAIN from homeassistant.components.traccar.device_tracker import ( PLATFORM_SCHEMA as TRACCAR_PLATFORM_SCHEMA, @@ -35,26 +37,39 @@ async def test_import_events_catch_all(hass): device = {"id": 1, "name": "abc123"} api_mock = AsyncMock() api_mock.devices = [device] - api_mock.get_events.return_value = [ - { - "deviceId": device["id"], - "type": "ignitionOn", - "serverTime": datetime.utcnow(), - "attributes": {}, - }, - { - "deviceId": device["id"], - "type": "ignitionOff", - "serverTime": datetime.utcnow(), - "attributes": {}, - }, + api_mock.get_reports_events.return_value = [ + ReportsEventeModel( + **{ + "id": 1, + "positionId": 1, + "geofenceId": 1, + "maintenanceId": 1, + "deviceId": device["id"], + "type": "ignitionOn", + "eventTime": datetime.utcnow().isoformat(), + "attributes": {}, + } + ), + ReportsEventeModel( + **{ + "id": 2, + "positionId": 2, + "geofenceId": 1, + "maintenanceId": 1, + "deviceId": device["id"], + "type": "ignitionOff", + "eventTime": datetime.utcnow().isoformat(), + "attributes": {}, + } + ), ] events_ignition_on = async_capture_events(hass, "traccar_ignition_on") events_ignition_off = async_capture_events(hass, "traccar_ignition_off") with patch( - "homeassistant.components.traccar.device_tracker.API", return_value=api_mock + "homeassistant.components.traccar.device_tracker.ApiClient", + return_value=api_mock, ): assert await async_setup_component(hass, DOMAIN, conf_dict) From c7ddc595ed7243be20e18b6f1b40dba5ea3e3fac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 01:09:59 +0200 Subject: [PATCH 688/820] Add issue to repairs for removed Spotify YAML configuration (#75736) * Add issue to repairs for removed Spotify YAML configuration * Tweak message --- homeassistant/components/spotify/__init__.py | 18 ++++++++++++++++++ homeassistant/components/spotify/manifest.json | 2 +- homeassistant/components/spotify/strings.json | 6 ++++++ .../components/spotify/translations/en.json | 6 ++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 7ee4e9649a5..83841a1780e 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -9,6 +9,7 @@ import aiohttp import requests from spotipy import Spotify, SpotifyException +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -18,6 +19,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .browse_media import async_browse_media @@ -51,6 +53,22 @@ class HomeAssistantSpotifyData: session: OAuth2Session +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Spotify integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Spotify from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 2940700d230..0556cad26b7 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/spotify", "requirements": ["spotipy==2.20.0"], "zeroconf": ["_spotify-connect._tcp.local."], - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "codeowners": ["@frenck"], "config_flow": true, "quality_scale": "silver", diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index caec5b8a288..4405bd21310 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -21,5 +21,11 @@ "info": { "api_endpoint_reachable": "Spotify API endpoint reachable" } + }, + "issues": { + "removed_yaml": { + "title": "The Spotify YAML configuration has been removed", + "description": "Configuring Spotify using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json index 7136e5a8e71..0ccebc9833b 100644 --- a/homeassistant/components/spotify/translations/en.json +++ b/homeassistant/components/spotify/translations/en.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Configuring Spotify using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Spotify YAML configuration has been removed" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API endpoint reachable" From 4ea532b3ea1a17e658a2b618eccc711cb394dd2c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 01:11:07 +0200 Subject: [PATCH 689/820] Add issue to repairs for removed Steam YAML configuration (#75737) --- .../components/steam_online/__init__.py | 18 ++++++++++++++++++ .../components/steam_online/manifest.json | 1 + .../components/steam_online/strings.json | 6 ++++++ .../steam_online/translations/en.json | 6 ++++++ 4 files changed, 31 insertions(+) diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index eae81dd8435..c422269a277 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -1,11 +1,13 @@ """The Steam integration.""" from __future__ import annotations +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN @@ -14,6 +16,22 @@ from .coordinator import SteamDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Steam integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Steam from a config entry.""" coordinator = SteamDataUpdateCoordinator(hass) diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index f8aba1aee07..f2e3a35bbe7 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/steam_online", "requirements": ["steamodd==4.21"], + "dependencies": ["repairs"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["steam"] diff --git a/homeassistant/components/steam_online/strings.json b/homeassistant/components/steam_online/strings.json index 1b431795ea4..63dc7cce22a 100644 --- a/homeassistant/components/steam_online/strings.json +++ b/homeassistant/components/steam_online/strings.json @@ -35,5 +35,11 @@ "error": { "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" } + }, + "issues": { + "removed_yaml": { + "title": "The Steam YAML configuration has been removed", + "description": "Configuring Steam using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/steam_online/translations/en.json b/homeassistant/components/steam_online/translations/en.json index 7226c5ee177..2a7bef3f683 100644 --- a/homeassistant/components/steam_online/translations/en.json +++ b/homeassistant/components/steam_online/translations/en.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Configuring Steam using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Steam YAML configuration has been removed" + } + }, "options": { "error": { "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" From 7fd47717cf40afaa5464360b19f381564cc9fdc3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 26 Jul 2022 02:10:05 +0200 Subject: [PATCH 690/820] Bump goodwe to 0.2.18 (#75615) --- homeassistant/components/goodwe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/goodwe/manifest.json b/homeassistant/components/goodwe/manifest.json index 45895d0d4b0..c91b91c02a9 100644 --- a/homeassistant/components/goodwe/manifest.json +++ b/homeassistant/components/goodwe/manifest.json @@ -3,7 +3,7 @@ "name": "GoodWe Inverter", "documentation": "https://www.home-assistant.io/integrations/goodwe", "codeowners": ["@mletenay", "@starkillerOG"], - "requirements": ["goodwe==0.2.15"], + "requirements": ["goodwe==0.2.18"], "config_flow": true, "iot_class": "local_polling", "loggers": ["goodwe"] diff --git a/requirements_all.txt b/requirements_all.txt index fa189bc1d0f..526f5f29c71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -743,7 +743,7 @@ glances_api==0.3.5 goalzero==0.2.1 # homeassistant.components.goodwe -goodwe==0.2.15 +goodwe==0.2.18 # homeassistant.components.google_pubsub google-cloud-pubsub==2.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fca788e61e..a24245dfc3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -549,7 +549,7 @@ glances_api==0.3.5 goalzero==0.2.1 # homeassistant.components.goodwe -goodwe==0.2.15 +goodwe==0.2.18 # homeassistant.components.google_pubsub google-cloud-pubsub==2.11.0 From 9c725bc106aeba8320e8e855dc5cb0aaa1901784 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 26 Jul 2022 00:26:43 +0000 Subject: [PATCH 691/820] [ci skip] Translation update --- .../components/bluetooth/translations/ca.json | 12 ++++++++++- .../components/bluetooth/translations/de.json | 12 ++++++++++- .../components/bluetooth/translations/et.json | 12 ++++++++++- .../bluetooth/translations/pt-BR.json | 12 ++++++++++- .../bluetooth/translations/zh-Hant.json | 12 ++++++++++- .../components/govee_ble/translations/et.json | 21 +++++++++++++++++++ .../components/govee_ble/translations/hu.json | 21 +++++++++++++++++++ .../govee_ble/translations/zh-Hant.json | 21 +++++++++++++++++++ .../components/moat/translations/et.json | 21 +++++++++++++++++++ .../components/moat/translations/hu.json | 21 +++++++++++++++++++ .../components/moat/translations/zh-Hant.json | 21 +++++++++++++++++++ .../radiotherm/translations/ca.json | 6 ++++++ .../radiotherm/translations/de.json | 6 ++++++ .../radiotherm/translations/et.json | 6 ++++++ .../radiotherm/translations/pt-BR.json | 6 ++++++ .../radiotherm/translations/zh-Hant.json | 6 ++++++ .../simplisafe/translations/ca.json | 7 +++++-- .../simplisafe/translations/et.json | 7 +++++-- .../simplisafe/translations/hu.json | 5 ++++- .../simplisafe/translations/zh-Hant.json | 7 +++++-- .../components/switchbot/translations/ca.json | 3 ++- .../components/switchbot/translations/de.json | 2 +- .../components/switchbot/translations/et.json | 3 ++- .../components/switchbot/translations/hu.json | 1 + .../switchbot/translations/zh-Hant.json | 3 ++- .../xiaomi_ble/translations/ca.json | 15 +++++++++++++ .../xiaomi_ble/translations/de.json | 6 ++++++ .../xiaomi_ble/translations/et.json | 15 +++++++++++++ .../xiaomi_ble/translations/hu.json | 15 +++++++++++++ .../xiaomi_ble/translations/zh-Hant.json | 15 +++++++++++++ 30 files changed, 304 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/govee_ble/translations/et.json create mode 100644 homeassistant/components/govee_ble/translations/hu.json create mode 100644 homeassistant/components/govee_ble/translations/zh-Hant.json create mode 100644 homeassistant/components/moat/translations/et.json create mode 100644 homeassistant/components/moat/translations/hu.json create mode 100644 homeassistant/components/moat/translations/zh-Hant.json diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json index 8fb4c022e0a..082803a48dc 100644 --- a/homeassistant/components/bluetooth/translations/ca.json +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "no_adapters": "No s'ha trobat cap adaptador Bluetooth" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Tria un dispositiu a configurar" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Adaptador Bluetooth a utilitzar per escanejar" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index ef305862210..6e65b985478 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert", + "no_adapters": "Keine Bluetooth-Adapter gefunden" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Der zum Scannen zu verwendende Bluetooth-Adapter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json index 4098abefbf7..da1dbdb1a5f 100644 --- a/homeassistant/components/bluetooth/translations/et.json +++ b/homeassistant/components/bluetooth/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "no_adapters": "Bluetoothi adaptereid ei leitud" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Vali h\u00e4\u00e4lestatav seade" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Sk\u00e4nnimiseks kasutatav Bluetoothi adapter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index 05ddee20ecb..0a5cf354495 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "no_adapters": "Nenhum adaptador Bluetooth encontrado" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Escolha um dispositivo para configurar" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "O adaptador Bluetooth a ser usado para escaneamento" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index 7d69eb15ac1..34ab75775ab 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_adapters": "\u627e\u4e0d\u5230\u4efb\u4f55\u85cd\u82bd\u50b3\u8f38\u5668" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u82bd\u50b3\u8f38\u5668" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/et.json b/homeassistant/components/govee_ble/translations/et.json new file mode 100644 index 00000000000..8dc1b9f6ed0 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/hu.json b/homeassistant/components/govee_ble/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/zh-Hant.json b/homeassistant/components/govee_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/govee_ble/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/et.json b/homeassistant/components/moat/translations/et.json new file mode 100644 index 00000000000..8dc1b9f6ed0 --- /dev/null +++ b/homeassistant/components/moat/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/hu.json b/homeassistant/components/moat/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/moat/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/zh-Hant.json b/homeassistant/components/moat/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/moat/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ca.json b/homeassistant/components/radiotherm/translations/ca.json index d1249ddd23f..58e8487607f 100644 --- a/homeassistant/components/radiotherm/translations/ca.json +++ b/homeassistant/components/radiotherm/translations/ca.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de la plataforma 'Radio Thermostat' mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "S'est\u00e0 eliminant la configuraci\u00f3 YAML de Thermostat Radio" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/de.json b/homeassistant/components/radiotherm/translations/de.json index 50315afce52..2845d3d6190 100644 --- a/homeassistant/components/radiotherm/translations/de.json +++ b/homeassistant/components/radiotherm/translations/de.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration der Radiothermostat-Klimaplattform mit YAML wird in Home Assistant 2022.9 entfernt. \n\nDeine vorhandene Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die YAML-Konfiguration des Funkthermostats wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/et.json b/homeassistant/components/radiotherm/translations/et.json index f8a6a7303ab..8eefd4125bb 100644 --- a/homeassistant/components/radiotherm/translations/et.json +++ b/homeassistant/components/radiotherm/translations/et.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Raadiotermostaadi kliimaplatvormi konfigureerimine YAML-i abil eemaldatakse rakenduses Home Assistant 2022.9. \n\n Olemasolev konfiguratsioon imporditi kasutajaliidesesse automaatselt. Selle probleemi lahendamiseks eemalda YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivita Home Assistant.", + "title": "Raadiotermostaadi YAML-i konfiguratsioon eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/pt-BR.json b/homeassistant/components/radiotherm/translations/pt-BR.json index da10f6bd457..de9b3f092cc 100644 --- a/homeassistant/components/radiotherm/translations/pt-BR.json +++ b/homeassistant/components/radiotherm/translations/pt-BR.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o da plataforma clim\u00e1tica Radio Thermostat usando YAML est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Sua configura\u00e7\u00e3o existente foi importada para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do r\u00e1dio termostato est\u00e1 sendo removida" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/zh-Hant.json b/homeassistant/components/radiotherm/translations/zh-Hant.json index 7a5a40817f7..ad1af3bb442 100644 --- a/homeassistant/components/radiotherm/translations/zh-Hant.json +++ b/homeassistant/components/radiotherm/translations/zh-Hant.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Radio \u6eab\u63a7\u5668\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Radio \u6eab\u63a7\u5668 YAML \u8a2d\u5b9a\u5df2\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index a2fd356932c..dbb3d0e4207 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Aquest compte SimpliSafe ja est\u00e0 en \u00fas.", "email_2fa_timed_out": "S'ha esgotat el temps d'espera de l'autenticaci\u00f3 de dos factors a trav\u00e9s de correu electr\u00f2nic.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "wrong_account": "Les credencials d'usuari proporcionades no coincideixen amb les d'aquest compte SimpliSafe." }, "error": { + "identifier_exists": "Compte ja est\u00e0 registrat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Codi d'autoritzaci\u00f3", "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Introdueix el teu nom d'usuari i contrasenya." + "description": "SimpliSafe autentica els seus usuaris a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) abans de comen\u00e7ar.\n\nQuan ja estiguis, fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials. Quan el proc\u00e9s s'hagi completat, torna aqu\u00ed i introdueix, a sota, el codi d'autoritzaci\u00f3 de l'URL de l'aplicaci\u00f3 web de SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 0f01f4d9b9c..073d20555c1 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "See SimpliSafe'i konto on juba kasutusel.", "email_2fa_timed_out": "Meilip\u00f5hise kahefaktorilise autentimise ajal\u00f5pp.", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "wrong_account": "Esitatud kasutaja mandaadid ei \u00fchti selle SimpliSafe kontoga." }, "error": { + "identifier_exists": "Konto on juba registreeritud", "invalid_auth": "Tuvastamise viga", "unknown": "Tundmatu viga" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Tuvastuskood", "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "Sisesta kasutajatunnus ja salas\u00f5na" + "description": "SimpliSafe autendib kasutajaid oma veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus k\u00e4sitsi samm; palun veendu, et loed enne alustamist l\u00e4bi [dokumendid](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKui oled valmis, kl\u00f5psa veebirakenduse SimpliSafe avamiseks ja mandaadi sisestamiseks kl\u00f5psa [here]({url}). Kui protsess on l\u00f5pule j\u00f5udnud, naase siia ja sisesta autoriseerimiskood SimpliSafe veebirakenduse URL-ist." } } }, diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 12f745b3b69..ece2b0a0dfb 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Ez a SimpliSafe-fi\u00f3k m\u00e1r haszn\u00e1latban van.", "email_2fa_timed_out": "Az e-mail alap\u00fa k\u00e9tfaktoros hiteles\u00edt\u00e9sre val\u00f3 v\u00e1rakoz\u00e1s ideje lej\u00e1rt", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "wrong_account": "A megadott felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok nem j\u00f3k ehhez a SimpliSafe fi\u00f3khoz." }, "error": { + "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lt", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, @@ -28,6 +30,7 @@ }, "user": { "data": { + "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index ce763da538e..99f08eb14d4 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u6b64 SimpliSafe \u5e33\u865f\u5df2\u88ab\u4f7f\u7528\u3002", "email_2fa_timed_out": "\u7b49\u5f85\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u90f5\u4ef6\u903e\u6642\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "wrong_account": "\u6240\u4ee5\u63d0\u4f9b\u7684\u6191\u8b49\u8207 Simplisafe \u5e33\u865f\u4e0d\u7b26\u3002" }, "error": { + "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "\u8a8d\u8b49\u78bc", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002" + "description": "SimpliSafe \u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u5176\u4f7f\u7528\u8005\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3002\n\n\u6e96\u5099\u5c31\u7dd2\u5f8c\u3001\u9ede\u9078 [\u6b64\u8655]({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\u5b8c\u6210\u5f8c\u56de\u5230\u9019\u88e1\u4e26\u8f38\u5165\u7531 SimpliSafe Web App \u6240\u53d6\u7684\u8a8d\u8b49\u78bc\u3002" } } }, diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index 576aeda3b16..a3ba38e2f24 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Tipus de Switchbot no compatible.", "unknown": "Error inesperat" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Adre\u00e7a del dispositiu", "mac": "Adre\u00e7a MAC del dispositiu", "name": "Nom", "password": "Contrasenya" diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index c1ab660e6a5..ca306d6fa2d 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -7,7 +7,7 @@ "switchbot_unsupported_type": "Nicht unterst\u00fctzter Switchbot-Typ.", "unknown": "Unerwarteter Fehler" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index 358a4748724..a46ec376a96 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Toetamata Switchboti t\u00fc\u00fcp.", "unknown": "Ootamatu t\u00f5rge" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Seadme aadress", "mac": "Seadme MAC-aadress", "name": "Nimi", "password": "Salas\u00f5na" diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index b870e577426..e44bfe7c811 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -15,6 +15,7 @@ "step": { "user": { "data": { + "address": "Eszk\u00f6z c\u00edme", "mac": "Eszk\u00f6z MAC-c\u00edme", "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3" diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 617129167ed..6d14e05aff7 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u5225\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "\u88dd\u7f6e\u4f4d\u5740", "mac": "\u88dd\u7f6e MAC \u4f4d\u5740", "name": "\u540d\u7a31", "password": "\u5bc6\u78bc" diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index 0cd4571dc9d..1c24ab7172e 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "decryption_failed": "La clau d'enlla\u00e7 proporcionada no ha funcionat, les dades del sensor no s'han pogut desxifrar. Comprova-la i torna-ho a provar.", + "expected_24_characters": "S'espera una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals.", + "expected_32_characters": "S'espera una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals.", "no_devices_found": "No s'han trobat dispositius a la xarxa" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Vols configurar {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals." + }, "user": { "data": { "address": "Dispositiu" diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index b81386a076f..0d3e2c542d2 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -14,9 +14,15 @@ "description": "M\u00f6chtest du {name} einrichten?" }, "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindungsschl\u00fcssel" + }, "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 32-stelligen hexadezimalen Bindungsschl\u00fcssel." }, "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindungsschl\u00fcssel" + }, "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindungsschl\u00fcssel." }, "user": { diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json index 749f7e45de5..1895097e7b1 100644 --- a/homeassistant/components/xiaomi_ble/translations/et.json +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", + "decryption_failed": "Esitatud sidumisv\u00f5ti ei t\u00f6\u00f6tanud, sensori andmeid ei saanud dekr\u00fcpteerida. Palun kontrolli seda ja proovi uuesti.", + "expected_24_characters": "Eeldati 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", + "expected_32_characters": "Eeldati 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", "no_devices_found": "V\u00f6rgust seadmeid ei leitud" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Kas seadistada {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Sidumisv\u00f5ti" + }, + "description": "Anduri edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks vajame 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Sidumisv\u00f5ti" + }, + "description": "Anduri edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks vajame 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." + }, "user": { "data": { "address": "Seade" diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index 7ef0d3a6301..b03a41f60a6 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "expected_24_characters": "24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", + "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Kulcs (bindkey)" + }, + "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Kulcs (bindkey)" + }, + "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, "user": { "data": { "address": "Eszk\u00f6z" diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json index d4eaa8cb41f..81f7e2050af 100644 --- a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "decryption_failed": "\u6240\u63d0\u4f9b\u7684\u7d81\u5b9a\u78bc\u7121\u6cd5\u4f7f\u7528\u3001\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6cd5\u89e3\u5bc6\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "expected_24_characters": "\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", + "expected_32_characters": "\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "\u7d81\u5b9a\u78bc" + }, + "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "\u7d81\u5b9a\u78bc" + }, + "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" + }, "user": { "data": { "address": "\u88dd\u7f6e" From 2b617e3885d8a2d1b8c9299abc5fd1e547ccb5ac Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 26 Jul 2022 03:04:19 +0200 Subject: [PATCH 692/820] Improve mqtt MessageCallback typing (#75614) * Improve mqtt MessageCallback typing * Use MQTTMessage --- homeassistant/components/mqtt/client.py | 13 +++++++------ homeassistant/components/mqtt/models.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 81142fadb87..192de624f17 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Iterable +from collections.abc import Awaitable, Callable, Coroutine, Iterable from functools import lru_cache, partial, wraps import inspect from itertools import groupby @@ -15,6 +15,7 @@ import uuid import attr import certifi +from paho.mqtt.client import MQTTMessage from homeassistant.const import ( CONF_CLIENT_ID, @@ -246,7 +247,7 @@ class Subscription: topic: str = attr.ib() matcher: Any = attr.ib() - job: HassJob = attr.ib() + job: HassJob[[ReceiveMessage], Coroutine[Any, Any, None] | None] = attr.ib() qos: int = attr.ib(default=0) encoding: str | None = attr.ib(default="utf-8") @@ -444,7 +445,7 @@ class MQTT: async def async_subscribe( self, topic: str, - msg_callback: MessageCallbackType, + msg_callback: AsyncMessageCallbackType | MessageCallbackType, qos: int, encoding: str | None = None, ) -> Callable[[], None]: @@ -597,15 +598,15 @@ class MQTT: self.hass.add_job(self._mqtt_handle_message, msg) @lru_cache(2048) - def _matching_subscriptions(self, topic): - subscriptions = [] + def _matching_subscriptions(self, topic: str) -> list[Subscription]: + subscriptions: list[Subscription] = [] for subscription in self.subscriptions: if subscription.matcher(topic): subscriptions.append(subscription) return subscriptions @callback - def _mqtt_handle_message(self, msg) -> None: + def _mqtt_handle_message(self, msg: MQTTMessage) -> None: _LOGGER.debug( "Received message on %s%s: %s", msg.topic, diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index d5560f6954e..84bf704a262 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -2,7 +2,7 @@ from __future__ import annotations from ast import literal_eval -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine import datetime as dt from typing import Any, Union @@ -42,7 +42,7 @@ class ReceiveMessage: timestamp: dt.datetime = attr.ib(default=None) -AsyncMessageCallbackType = Callable[[ReceiveMessage], Awaitable[None]] +AsyncMessageCallbackType = Callable[[ReceiveMessage], Coroutine[Any, Any, None]] MessageCallbackType = Callable[[ReceiveMessage], None] From 1f73a553c803869d8baaf523a6a02b4d79bac809 Mon Sep 17 00:00:00 2001 From: qiz-li <47799695+qiz-li@users.noreply.github.com> Date: Mon, 25 Jul 2022 19:50:04 -0700 Subject: [PATCH 693/820] Bump Switchmate dependency to 0.5.1 (#75163) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 +- .../components/switchmate/manifest.json | 4 ++-- homeassistant/components/switchmate/switch.py | 16 ++++++++-------- requirements_all.txt | 2 +- script/gen_requirements_all.py | 1 - 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index bcbd6c20364..3ad4c23816c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1046,7 +1046,7 @@ build.json @home-assistant/supervisor /tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode -/homeassistant/components/switchmate/ @danielhiversen +/homeassistant/components/switchmate/ @danielhiversen @qiz-li /homeassistant/components/syncthing/ @zhulik /tests/components/syncthing/ @zhulik /homeassistant/components/syncthru/ @nielstron diff --git a/homeassistant/components/switchmate/manifest.json b/homeassistant/components/switchmate/manifest.json index c4a263aca19..930fb6bf88e 100644 --- a/homeassistant/components/switchmate/manifest.json +++ b/homeassistant/components/switchmate/manifest.json @@ -2,8 +2,8 @@ "domain": "switchmate", "name": "Switchmate SimplySmart Home", "documentation": "https://www.home-assistant.io/integrations/switchmate", - "requirements": ["pySwitchmate==0.4.6"], - "codeowners": ["@danielhiversen"], + "requirements": ["pySwitchmate==0.5.1"], + "codeowners": ["@danielhiversen", "@qiz-li"], "iot_class": "local_polling", "loggers": ["switchmate"] } diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index b0f2b58a1fa..7beb89f8de1 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta # pylint: disable=import-error -import switchmate +from switchmate import Switchmate import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -49,7 +49,7 @@ class SwitchmateEntity(SwitchEntity): self._mac = mac self._name = name - self._device = switchmate.Switchmate(mac=mac, flip_on_off=flip_on_off) + self._device = Switchmate(mac=mac, flip_on_off=flip_on_off) @property def unique_id(self) -> str: @@ -66,19 +66,19 @@ class SwitchmateEntity(SwitchEntity): """Return the name of the switch.""" return self._name - def update(self) -> None: + async def async_update(self) -> None: """Synchronize state with switch.""" - self._device.update() + await self._device.update() @property def is_on(self) -> bool: """Return true if it is on.""" return self._device.state - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the switch on.""" - self._device.turn_on() + await self._device.turn_on() - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn the switch off.""" - self._device.turn_off() + await self._device.turn_off() diff --git a/requirements_all.txt b/requirements_all.txt index 526f5f29c71..6aaa99d7838 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1357,7 +1357,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.switchmate -# pySwitchmate==0.4.6 +pySwitchmate==0.5.1 # homeassistant.components.tibber pyTibber==0.22.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index dcc53a73df0..5ef794b4eab 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -30,7 +30,6 @@ COMMENT_REQUIREMENTS = ( "opencv-python-headless", "pybluez", "pycups", - "pySwitchmate", "python-eq3bt", "python-gammu", "python-lirc", From ce4e53938cbee61244a22595526f10141dffe799 Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 26 Jul 2022 07:24:39 +0300 Subject: [PATCH 694/820] Change monoprice config flow to sync (#75306) --- homeassistant/components/monoprice/config_flow.py | 4 ++-- tests/components/monoprice/test_config_flow.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 4065b003ba3..9c659d4f733 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from pymonoprice import get_async_monoprice +from pymonoprice import get_monoprice from serial import SerialException import voluptuous as vol @@ -56,7 +56,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ try: - await get_async_monoprice(data[CONF_PORT], hass.loop) + await hass.async_add_executor_job(get_monoprice, data[CONF_PORT]) except SerialException as err: _LOGGER.error("Error connecting to Monoprice controller") raise CannotConnect from err diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index b88d4d240bd..c9f5bb9dee7 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -33,7 +33,7 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.monoprice.config_flow.get_async_monoprice", + "homeassistant.components.monoprice.config_flow.get_monoprice", return_value=True, ), patch( "homeassistant.components.monoprice.async_setup_entry", @@ -60,7 +60,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.monoprice.config_flow.get_async_monoprice", + "homeassistant.components.monoprice.config_flow.get_monoprice", side_effect=SerialException, ): result2 = await hass.config_entries.flow.async_configure( @@ -78,7 +78,7 @@ async def test_generic_exception(hass): ) with patch( - "homeassistant.components.monoprice.config_flow.get_async_monoprice", + "homeassistant.components.monoprice.config_flow.get_monoprice", side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( From b57e0d13b454b9ae7077f291de90b05491624b4b Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 26 Jul 2022 08:50:21 +0200 Subject: [PATCH 695/820] Fix Epson wrong volume value (#75264) --- homeassistant/components/epson/manifest.json | 2 +- homeassistant/components/epson/media_player.py | 7 ++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 310b66c0d37..82b74486377 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -3,7 +3,7 @@ "name": "Epson", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/epson", - "requirements": ["epson-projector==0.4.2"], + "requirements": ["epson-projector==0.4.6"], "codeowners": ["@pszafer"], "iot_class": "local_polling", "loggers": ["epson_projector"] diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index f72b0f69d69..98152efb3b2 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -133,7 +133,10 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): self._source = SOURCE_LIST.get(source, self._source) volume = await self._projector.get_property(VOLUME) if volume: - self._volume = volume + try: + self._volume = float(volume) + except ValueError: + self._volume = None elif power_state == BUSY: self._state = STATE_ON else: @@ -176,11 +179,13 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): """Turn on epson.""" if self._state == STATE_OFF: await self._projector.send_command(TURN_ON) + self._state = STATE_ON async def async_turn_off(self): """Turn off epson.""" if self._state == STATE_ON: await self._projector.send_command(TURN_OFF) + self._state = STATE_OFF @property def source_list(self): diff --git a/requirements_all.txt b/requirements_all.txt index 6aaa99d7838..3804872c58e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -619,7 +619,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.2 +epson-projector==0.4.6 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a24245dfc3a..1fae9c42e30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -465,7 +465,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.2 +epson-projector==0.4.6 # homeassistant.components.faa_delays faadelays==0.0.7 From 5e45b0baf977be00743170962ce7701021bb9a32 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 09:03:31 +0200 Subject: [PATCH 696/820] Automatically set up Xiaomi BLE during onboarding (#75748) --- .../components/xiaomi_ble/config_flow.py | 3 ++- .../components/xiaomi_ble/test_config_flow.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 8b3ec22def7..e7c4a3e1f8c 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData from xiaomi_ble.parser import EncryptionScheme +from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothServiceInfo, async_discovered_service_info, @@ -139,7 +140,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry( title=self.context["title_placeholders"]["name"], data={}, diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index fc625bdf7ec..d4b4300d2c1 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -38,6 +38,28 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" +async def test_async_step_bluetooth_during_onboarding(hass): + """Test discovery via bluetooth during onboarding.""" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "MMC_T201_1" + assert result["data"] == {} + assert result["result"].unique_id == "00:81:F9:DD:6F:C1" + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + async def test_async_step_bluetooth_valid_device_legacy_encryption(hass): """Test discovery via bluetooth with a valid device, with legacy encryption.""" result = await hass.config_entries.flow.async_init( From 47713d968620f146251d10032ff2c84c0fc3e3ae Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 26 Jul 2022 00:52:15 -0700 Subject: [PATCH 697/820] Raise issue for Google Calendar YAML deprecations (#75743) --- homeassistant/components/google/__init__.py | 29 ++++++++++++------- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/google/strings.json | 10 +++++++ .../components/google/translations/en.json | 10 +++++++ 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index e05de4f5b79..4b72aaa77ad 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -20,6 +20,7 @@ from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, @@ -203,21 +204,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: }, ) ) - - _LOGGER.warning( - "Configuration of Google Calendar in YAML in configuration.yaml is " - "is deprecated and will be removed in a future release; Your existing " - "OAuth Application Credentials and access settings have been imported " - "into the UI automatically and can be safely removed from your " - "configuration.yaml file" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", # Warning first added in 2022.6.0 + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", ) if conf.get(CONF_TRACK_NEW) is False: # The track_new as False would previously result in new entries - # in google_calendars.yaml with track set to Fasle which is + # in google_calendars.yaml with track set to False which is # handled at calendar entity creation time. - _LOGGER.warning( - "You must manually set the integration System Options in the " - "UI to disable newly discovered entities going forward" + async_create_issue( + hass, + DOMAIN, + "removed_track_new_yaml", + breaks_in_ha_version="2022.6.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_track_new_yaml", ) return True diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d39f2093cf0..bf745a72927 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -2,7 +2,7 @@ "domain": "google", "name": "Google Calendars", "config_flow": true, - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": ["gcal-sync==0.10.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index b4c5270e003..58e5cedd98d 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -41,5 +41,15 @@ }, "application_credentials": { "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" + }, + "issues": { + "deprecated_yaml": { + "title": "The Google Calendar YAML configuration is being removed", + "description": "Configuring the Google Calendar in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_track_new_yaml": { + "title": "Google Calendar entity tracking has changed", + "description": "You have disabled entity tracking for Google Calendar in configuration.yaml, which is no longer supported. You must manually change the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 1720e8c1454..bd1769fcbe0 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Google Calendar in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Google Calendar YAML configuration is being removed" + }, + "removed_track_new_yaml": { + "description": "Your Google Calendar configuration.yaml has disabled new entity tracking, which is no longer supported. You must manually set the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue.", + "title": "The Google Calendar entity tracking has changed" + } + }, "options": { "step": { "init": { From a98f658854682ec1908d1b5b179032ca1cfb3ab0 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 26 Jul 2022 10:32:26 +0200 Subject: [PATCH 698/820] Update xknx to 0.22.0 (#75749) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/conftest.py | 12 ++++++++---- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 0c34428f0a1..4197cb76209 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.21.5"], + "requirements": ["xknx==0.22.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 3804872c58e..425b114231a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xboxapi==2.0.1 xiaomi-ble==0.5.1 # homeassistant.components.knx -xknx==0.21.5 +xknx==0.22.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1fae9c42e30..3c365f1fc85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.5.1 # homeassistant.components.knx -xknx==0.21.5 +xknx==0.22.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index ccfd3a35085..e5cf18b0c3c 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -55,19 +55,23 @@ class KNXTestKit: async def setup_integration(self, config): """Create the KNX integration.""" + def disable_rate_limiter(): + """Disable rate limiter for tests.""" + # after XKNX.__init__() to not overwrite it by the config entry again + # before StateUpdater starts to avoid slow down of tests + self.xknx.rate_limit = 0 + def knx_ip_interface_mock(): """Create a xknx knx ip interface mock.""" mock = Mock() - mock.start = AsyncMock() + mock.start = AsyncMock(side_effect=disable_rate_limiter) mock.stop = AsyncMock() mock.send_telegram = AsyncMock(side_effect=self._outgoing_telegrams.put) return mock def fish_xknx(*args, **kwargs): """Get the XKNX object from the constructor call.""" - self.xknx = kwargs["xknx"] - # disable rate limiter for tests (before StateUpdater starts) - self.xknx.rate_limit = 0 + self.xknx = args[0] return DEFAULT with patch( From eaee923e4cde4d6ed9a1fcf31f66484b063dabe6 Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Tue, 26 Jul 2022 10:47:03 +0200 Subject: [PATCH 699/820] Fix hvv departures authentication (#75146) --- homeassistant/components/hvv_departures/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hvv_departures/manifest.json b/homeassistant/components/hvv_departures/manifest.json index f0334b5af92..eb7be8b8c64 100644 --- a/homeassistant/components/hvv_departures/manifest.json +++ b/homeassistant/components/hvv_departures/manifest.json @@ -3,7 +3,7 @@ "name": "HVV Departures", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hvv_departures", - "requirements": ["pygti==0.9.2"], + "requirements": ["pygti==0.9.3"], "codeowners": ["@vigonotion"], "iot_class": "cloud_polling", "loggers": ["pygti"] diff --git a/requirements_all.txt b/requirements_all.txt index 425b114231a..47afd92827c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1549,7 +1549,7 @@ pygatt[GATTTOOL]==4.0.5 pygtfs==0.1.6 # homeassistant.components.hvv_departures -pygti==0.9.2 +pygti==0.9.3 # homeassistant.components.version pyhaversion==22.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c365f1fc85..32d8b2bf514 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1055,7 +1055,7 @@ pyfronius==0.7.1 pyfttt==0.3 # homeassistant.components.hvv_departures -pygti==0.9.2 +pygti==0.9.3 # homeassistant.components.version pyhaversion==22.4.1 From 0006629ca23455eb3d16ad28774660a3e84a7a45 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 26 Jul 2022 12:26:31 +0200 Subject: [PATCH 700/820] Fix small type issue [synology_dsm] (#75762) --- homeassistant/components/synology_dsm/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 89bbc4ae8c2..0314165eb41 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -383,7 +383,7 @@ class SynologyDSMOptionsFlowHandler(OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) -def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str) -> str: +def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) -> str: """Login to the NAS and fetch basic data.""" # These do i/o api.login(otp_code) From 5cb4bbd9067b5538552f73ca0b963e4976fa6666 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Jul 2022 04:17:28 -1000 Subject: [PATCH 701/820] Fix min and max mireds with HKC (#75744) --- .../components/homekit_controller/light.py | 12 + .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/nanoleaf_strip_nl55.json | 354 ++++++++++++++++++ .../test_nanoleaf_strip_nl55.py | 55 +++ 6 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json create mode 100644 tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 4073c1ac9fe..df691ac3f6f 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -71,6 +71,18 @@ class HomeKitLight(HomeKitEntity, LightEntity): self.service.value(CharacteristicsTypes.SATURATION), ) + @property + def min_mireds(self) -> int: + """Return minimum supported color temperature.""" + min_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].minValue + return int(min_value) if min_value else super().min_mireds + + @property + def max_mireds(self) -> int: + """Return the maximum color temperature.""" + max_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].maxValue + return int(max_value) if max_value else super().max_mireds + @property def color_temp(self) -> int: """Return the color temperature.""" diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 51d753f77fc..2de2a915d41 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.0"], + "requirements": ["aiohomekit==1.2.2"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 47afd92827c..bbf8e44cc80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.0 +aiohomekit==1.2.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32d8b2bf514..b383bce296f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.0 +aiohomekit==1.2.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json b/tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json new file mode 100644 index 00000000000..142af6c03a2 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json @@ -0,0 +1,354 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "Nanoleaf", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "NL55", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "Nanoleaf Strip 3B32", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "AAAA011111111111", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "1.4.40", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.2.4", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 9, + "perms": ["pr", "hd"], + "format": "string", + "value": "5.0;dfeceb3a", + "maxLen": 64 + }, + { + "type": "00000220-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "hd"], + "format": "data", + "value": "cf0c2e5a4476e152" + } + ] + }, + { + "iid": 11, + "type": "00000055-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000004C-0000-1000-8000-0026BB765291", + "iid": 34, + "perms": [], + "format": "data", + "description": "Pair Setup" + }, + { + "type": "0000004E-0000-1000-8000-0026BB765291", + "iid": 35, + "perms": [], + "format": "data", + "description": "Pair Verify" + }, + { + "type": "0000004F-0000-1000-8000-0026BB765291", + "iid": 36, + "perms": [], + "format": "uint8", + "description": "Pairing Features" + }, + { + "type": "00000050-0000-1000-8000-0026BB765291", + "iid": 37, + "perms": ["pr", "pw"], + "format": "data", + "value": null, + "description": "Pairing Pairings" + } + ] + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr"], + "format": "string", + "value": "2.2.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 19, + "type": "00000043-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 49, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 51, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 50, + "perms": ["pr"], + "format": "string", + "value": "Nanoleaf Light Strip", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "iid": 52, + "perms": ["pr", "pw", "ev"], + "format": "int", + "value": 100, + "description": "Brightness", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "000000CE-0000-1000-8000-0026BB765291", + "iid": 55, + "perms": ["pr", "pw", "ev"], + "format": "uint32", + "value": 470, + "description": "Color Temperature", + "minValue": 153, + "maxValue": 470, + "minStep": 1 + }, + { + "type": "00000013-0000-1000-8000-0026BB765291", + "iid": 53, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 30.0, + "description": "Hue", + "unit": "arcdegrees", + "minValue": 0.0, + "maxValue": 360.0, + "minStep": 1.0 + }, + { + "type": "0000002F-0000-1000-8000-0026BB765291", + "iid": 54, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 89.0, + "description": "Saturation", + "unit": "percentage", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 1.0 + }, + { + "type": "A28E1902-CFA1-4D37-A10F-0071CEEEEEBD", + "iid": 60, + "perms": ["pr", "pw", "hd"], + "format": "data", + "value": "" + }, + { + "type": "00000143-0000-1000-8000-0026BB765291", + "iid": 56, + "perms": ["pr", "pw"], + "format": "data", + "value": "" + }, + { + "type": "00000144-0000-1000-8000-0026BB765291", + "iid": 57, + "perms": ["pr"], + "format": "data", + "value": "010901013402040100000000000109010137020402000000" + }, + { + "type": "0000024B-0000-1000-8000-0026BB765291", + "iid": 58, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 2 + } + ] + }, + { + "iid": 31, + "type": "00000701-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 113, + "perms": ["pr"], + "format": "data", + "value": null + }, + { + "type": "00000706-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr"], + "format": "string", + "value": "", + "maxLen": 64 + }, + { + "type": "00000702-0000-1000-8000-0026BB765291", + "iid": 115, + "perms": ["pr"], + "format": "uint16", + "value": 31, + "description": "Thread Node Capabilities", + "minValue": 0, + "maxValue": 31 + }, + { + "type": "00000703-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "uint16", + "value": 127, + "description": "Thread Status", + "minValue": 0, + "maxValue": 127 + }, + { + "type": "0000022B-0000-1000-8000-0026BB765291", + "iid": 118, + "perms": ["pr"], + "format": "bool", + "value": false + }, + { + "type": "00000704-0000-1000-8000-0026BB765291", + "iid": 119, + "perms": ["pr", "pw"], + "format": "data", + "value": null + } + ] + }, + { + "iid": 38, + "type": "00000239-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 2564, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "0000023A-0000-1000-8000-0026BB765291", + "iid": 2561, + "perms": ["pr"], + "format": "uint32", + "value": 0, + "minValue": 0, + "maxValue": 67108863 + }, + { + "type": "0000023C-0000-1000-8000-0026BB765291", + "iid": 2562, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "0000024A-0000-1000-8000-0026BB765291", + "iid": 2565, + "perms": ["pr", "ev"], + "format": "uint32", + "value": 1 + } + ] + }, + { + "iid": 43, + "type": "0E9CC677-A71F-8B83-B84D-568278790CB3", + "characteristics": [] + }, + { + "iid": 44, + "type": "6D2AE1C4-9AEA-11EA-BB37-0242AC130002", + "characteristics": [] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py new file mode 100644 index 00000000000..7e6a9bb672b --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -0,0 +1,55 @@ +"""Make sure that Nanoleaf NL55 works with BLE.""" + +from homeassistant.helpers.entity import EntityCategory + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + +LIGHT_ON = ("lightbulb", "on") + + +async def test_nanoleaf_nl55_setup(hass): + """Test that a Nanoleaf NL55 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "nanoleaf_strip_nl55.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="Nanoleaf Strip 3B32", + model="NL55", + manufacturer="Nanoleaf", + sw_version="1.4.40", + hw_version="1.2.4", + serial_number="AAAA011111111111", + devices=[], + entities=[ + EntityTestInfo( + entity_id="light.nanoleaf_strip_3b32_nanoleaf_light_strip", + friendly_name="Nanoleaf Strip 3B32 Nanoleaf Light Strip", + unique_id="homekit-AAAA011111111111-19", + supported_features=0, + capabilities={ + "max_mireds": 470, + "min_mireds": 153, + "supported_color_modes": ["color_temp", "hs"], + }, + state="on", + ), + EntityTestInfo( + entity_id="button.nanoleaf_strip_3b32_identify", + friendly_name="Nanoleaf Strip 3B32 Identify", + unique_id="homekit-AAAA011111111111-aid:1-sid:1-cid:2", + entity_category=EntityCategory.DIAGNOSTIC, + state="unknown", + ), + ], + ), + ) From 9ad273a59f57161ee84ac90f489ae017076f851a Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 26 Jul 2022 22:27:16 +0800 Subject: [PATCH 702/820] Fix entity typo (#75767) --- homeassistant/components/radio_browser/__init__.py | 2 +- tests/components/mqtt/test_cover.py | 10 +++++----- tests/components/mqtt/test_init.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/radio_browser/__init__.py b/homeassistant/components/radio_browser/__init__.py index 89c2f220159..d93d7c48823 100644 --- a/homeassistant/components/radio_browser/__init__.py +++ b/homeassistant/components/radio_browser/__init__.py @@ -15,7 +15,7 @@ from .const import DOMAIN async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Radio Browser from a config entry. - This integration doesn't set up any enitites, as it provides a media source + This integration doesn't set up any entities, as it provides a media source only. """ session = async_get_clientsession(hass) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 3f85d4e89b1..b1c162073ed 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -987,7 +987,7 @@ async def test_set_tilt_templated_and_attributes( "set_position_topic": "set-position-topic", "set_position_template": "{{position-1}}", "tilt_command_template": "{" - '"enitity_id": "{{ entity_id }}",' + '"entity_id": "{{ entity_id }}",' '"value": {{ value }},' '"tilt_position": {{ tilt_position }}' "}", @@ -1009,7 +1009,7 @@ async def test_set_tilt_templated_and_attributes( mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 45,"tilt_position": 45}', + '{"entity_id": "cover.test","value": 45,"tilt_position": 45}', 0, False, ) @@ -1023,7 +1023,7 @@ async def test_set_tilt_templated_and_attributes( ) mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 100,"tilt_position": 100}', + '{"entity_id": "cover.test","value": 100,"tilt_position": 100}', 0, False, ) @@ -1037,7 +1037,7 @@ async def test_set_tilt_templated_and_attributes( ) mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 0,"tilt_position": 0}', + '{"entity_id": "cover.test","value": 0,"tilt_position": 0}', 0, False, ) @@ -1051,7 +1051,7 @@ async def test_set_tilt_templated_and_attributes( ) mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 100,"tilt_position": 100}', + '{"entity_id": "cover.test","value": 100,"tilt_position": 100}', 0, False, ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 1f263d40fc2..fe8f483adf2 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -281,7 +281,7 @@ async def test_command_template_value(hass): async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config): - """Test the rendering of enitity_variables.""" + """Test the rendering of entity variables.""" topic = "test/select" fake_state = ha.State("select.test", "milk") From af7df260a0dad192bc4790176f1120cd034a8230 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:28:22 +0200 Subject: [PATCH 703/820] Fix small type issues [core] (#75760) --- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/components/http/static.py | 2 +- homeassistant/config.py | 1 + homeassistant/helpers/event.py | 6 +++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 464ce495050..1de6c38aecf 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -320,6 +320,7 @@ class NotifySetupFlow(SetupFlow): errors: dict[str, str] = {} hass = self._auth_module.hass + assert self._secret and self._count if user_input: verified = await hass.async_add_executor_job( _verify_otp, self._secret, user_input["code"], self._count @@ -334,7 +335,6 @@ class NotifySetupFlow(SetupFlow): errors["base"] = "invalid_code" # generate code every time, no retry logic - assert self._secret and self._count code = await hass.async_add_executor_job( _generate_otp, self._secret, self._count ) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index e5e84ca141d..c4dc97727a9 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -23,7 +23,7 @@ PATH_CACHE = LRU(512) def _get_file_path( - filename: str, directory: Path, follow_symlinks: bool + filename: str | Path, directory: Path, follow_symlinks: bool ) -> Path | None: filepath = directory.joinpath(filename).resolve() if not follow_symlinks: diff --git a/homeassistant/config.py b/homeassistant/config.py index 5c870918231..91f94bbbf40 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -286,6 +286,7 @@ async def async_create_default_config(hass: HomeAssistant) -> bool: Return if creation was successful. """ + assert hass.config.config_dir return await hass.async_add_executor_job( _write_default_config, hass.config.config_dir ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 85cd684fca1..d18af953ec6 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -141,7 +141,7 @@ def threaded_listener_factory( def async_track_state_change( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[str, State, State], Awaitable[None] | None], + action: Callable[[str, State | None, State], Awaitable[None] | None], from_state: None | str | Iterable[str] = None, to_state: None | str | Iterable[str] = None, ) -> CALLBACK_TYPE: @@ -197,9 +197,9 @@ def async_track_state_change( """Handle specific state changes.""" hass.async_run_hass_job( job, - event.data.get("entity_id"), + event.data["entity_id"], event.data.get("old_state"), - event.data.get("new_state"), + event.data["new_state"], ) @callback From 516324ff540f904ea39c1517f8870c0f73d2ebff Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:30:12 +0200 Subject: [PATCH 704/820] Fix small type issue [fritz] (#75761) --- homeassistant/components/fritz/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index ddc09cb73a9..ea6461cef32 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -90,7 +90,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_check_configured_entry(self) -> ConfigEntry | None: """Check if entry is configured.""" - + assert self._host current_host = await self.hass.async_add_executor_job( socket.gethostbyname, self._host ) From 7d895c79e821b450748f73a96990dad504dfc399 Mon Sep 17 00:00:00 2001 From: Jevgeni Kiski Date: Tue, 26 Jul 2022 17:58:22 +0300 Subject: [PATCH 705/820] Bump vallox-websocket-api to 2.12.0 (#75734) --- homeassistant/components/vallox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 71b0750e2f2..5c862562fc1 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -2,7 +2,7 @@ "domain": "vallox", "name": "Vallox", "documentation": "https://www.home-assistant.io/integrations/vallox", - "requirements": ["vallox-websocket-api==2.11.0"], + "requirements": ["vallox-websocket-api==2.12.0"], "codeowners": ["@andre-richter", "@slovdahl", "@viiru-"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index bbf8e44cc80..c57f5c350af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2398,7 +2398,7 @@ uscisstatus==0.1.1 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.11.0 +vallox-websocket-api==2.12.0 # homeassistant.components.rdw vehicle==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b383bce296f..b0773fc089a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1607,7 +1607,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.11.0 +vallox-websocket-api==2.12.0 # homeassistant.components.rdw vehicle==0.4.0 From e6802f4f7e616f1f7605b388ac8d5167b87badfa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Jul 2022 05:14:02 -1000 Subject: [PATCH 706/820] Add support for switchbot contact/door sensor (#75730) --- .../components/switchbot/__init__.py | 2 ++ .../components/switchbot/binary_sensor.py | 27 ++++++++++++++++++- homeassistant/components/switchbot/const.py | 2 ++ .../components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/sensor.py | 5 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 46d6755553a..42ca0856b02 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -19,6 +19,7 @@ from homeassistant.helpers import device_registry as dr from .const import ( ATTR_BOT, + ATTR_CONTACT, ATTR_CURTAIN, ATTR_HYGROMETER, CONF_RETRY_COUNT, @@ -31,6 +32,7 @@ PLATFORMS_BY_TYPE = { ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], + ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], } CLASS_BY_DEVICE = { ATTR_CURTAIN: switchbot.SwitchbotCurtain, diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index d644f603697..4da4ed531b0 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -20,8 +21,30 @@ PARALLEL_UPDATES = 1 BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "calibration": BinarySensorEntityDescription( key="calibration", + name="Calibration", entity_category=EntityCategory.DIAGNOSTIC, ), + "motion_detected": BinarySensorEntityDescription( + key="pir_state", + name="Motion detected", + device_class=BinarySensorDeviceClass.MOTION, + ), + "contact_open": BinarySensorEntityDescription( + key="contact_open", + name="Door open", + device_class=BinarySensorDeviceClass.DOOR, + ), + "contact_timeout": BinarySensorEntityDescription( + key="contact_timeout", + name="Door timeout", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + "is_light": BinarySensorEntityDescription( + key="is_light", + name="Light", + device_class=BinarySensorDeviceClass.LIGHT, + ), } @@ -50,6 +73,8 @@ async def async_setup_entry( class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): """Representation of a Switchbot binary sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, @@ -62,8 +87,8 @@ class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): super().__init__(coordinator, unique_id, mac, name=switchbot_name) self._sensor = binary_sensor self._attr_unique_id = f"{unique_id}-{binary_sensor}" - self._attr_name = f"{switchbot_name} {binary_sensor.title()}" self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] + self._attr_name = self.entity_description.name @property def is_on(self) -> bool: diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index e9602a19048..dc5abc139e6 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -6,11 +6,13 @@ MANUFACTURER = "switchbot" ATTR_BOT = "bot" ATTR_CURTAIN = "curtain" ATTR_HYGROMETER = "hygrometer" +ATTR_CONTACT = "contact" DEFAULT_NAME = "Switchbot" SUPPORTED_MODEL_TYPES = { "WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN, "WoSensorTH": ATTR_HYGROMETER, + "WoContact": ATTR_CONTACT, } # Config Defaults diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 3bccbb4f674..d6acb69431e 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.15.1"], + "requirements": ["PySwitchbot==0.15.2"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 25863a57df5..f796ea05e7b 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -36,21 +37,25 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key="battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), "lightLevel": SensorEntityDescription( key="lightLevel", native_unit_of_measurement="Level", + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.ILLUMINANCE, ), "humidity": SensorEntityDescription( key="humidity", native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.HUMIDITY, ), "temperature": SensorEntityDescription( key="temperature", native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, ), } diff --git a/requirements_all.txt b/requirements_all.txt index c57f5c350af..2fa86ae9053 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.1 +PySwitchbot==0.15.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0773fc089a..bd79723fc79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.1 +PySwitchbot==0.15.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 3ee0ca8550beca5031c028cc4c78ab293d69fe22 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 17:35:47 +0200 Subject: [PATCH 707/820] Raise repair issue for Mi Flora (#75752) --- .../components/miflora/manifest.json | 6 +- homeassistant/components/miflora/sensor.py | 238 +----------------- homeassistant/components/miflora/strings.json | 8 + .../components/miflora/translations/en.json | 8 + requirements_all.txt | 4 - 5 files changed, 32 insertions(+), 232 deletions(-) create mode 100644 homeassistant/components/miflora/strings.json create mode 100644 homeassistant/components/miflora/translations/en.json diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index eea4b2b82fe..faae8fb140e 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -2,8 +2,8 @@ "domain": "miflora", "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", - "requirements": ["bluepy==1.3.0", "miflora==0.7.2"], + "requirements": [], + "dependencies": ["repairs"], "codeowners": ["@danielhiversen", "@basnijholt"], - "iot_class": "local_polling", - "loggers": ["btlewrap", "miflora"] + "iot_class": "local_polling" } diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 2dd819e45c5..76808e9706c 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -1,113 +1,13 @@ """Support for Xiaomi Mi Flora BLE plant sensor.""" from __future__ import annotations -from datetime import timedelta -import logging -from typing import Any - -import btlewrap -from btlewrap import BluetoothBackendException -from miflora import miflora_poller -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONDUCTIVITY, - CONF_FORCE_UPDATE, - CONF_MAC, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, - LIGHT_LUX, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.dt as dt_util -try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - - BACKEND = btlewrap.BluepyBackend -except ImportError: - BACKEND = btlewrap.GatttoolBackend - -_LOGGER = logging.getLogger(__name__) - -CONF_ADAPTER = "adapter" -CONF_MEDIAN = "median" -CONF_GO_UNAVAILABLE_TIMEOUT = "go_unavailable_timeout" - -DEFAULT_ADAPTER = "hci0" -DEFAULT_FORCE_UPDATE = False -DEFAULT_MEDIAN = 3 -DEFAULT_NAME = "Mi Flora" -DEFAULT_GO_UNAVAILABLE_TIMEOUT = timedelta(seconds=7200) - -SCAN_INTERVAL = timedelta(seconds=1200) - -ATTR_LAST_SUCCESSFUL_UPDATE = "last_successful_update" - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="temperature", - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key="light", - name="Light intensity", - native_unit_of_measurement=LIGHT_LUX, - device_class=SensorDeviceClass.ILLUMINANCE, - ), - SensorEntityDescription( - key="moisture", - name="Moisture", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:water-percent", - ), - SensorEntityDescription( - key="conductivity", - name="Conductivity", - native_unit_of_measurement=CONDUCTIVITY, - icon="mdi:lightning-bolt-circle", - ), - SensorEntityDescription( - key="battery", - name="Battery", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.BATTERY, - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, - vol.Optional( - CONF_GO_UNAVAILABLE_TIMEOUT, default=DEFAULT_GO_UNAVAILABLE_TIMEOUT - ): cv.time_period, - } -) +PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE async def async_setup_platform( @@ -117,125 +17,13 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the MiFlora sensor.""" - backend = BACKEND - _LOGGER.debug("Miflora is using %s backend", backend.__name__) - - cache = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL).total_seconds() - poller = miflora_poller.MiFloraPoller( - config[CONF_MAC], - cache_timeout=cache, - adapter=config[CONF_ADAPTER], - backend=backend, + async_create_issue( + hass, + "miflora", + "replaced", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="replaced", + learn_more_url="https://www.home-assistant.io/integrations/xiaomi_ble/", ) - force_update = config[CONF_FORCE_UPDATE] - median = config[CONF_MEDIAN] - - go_unavailable_timeout = config[CONF_GO_UNAVAILABLE_TIMEOUT] - - prefix = config[CONF_NAME] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - MiFloraSensor( - description, - poller, - prefix, - force_update, - median, - go_unavailable_timeout, - ) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - async_add_entities(entities) - - -class MiFloraSensor(SensorEntity): - """Implementing the MiFlora sensor.""" - - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__( - self, - description: SensorEntityDescription, - poller, - prefix, - force_update, - median, - go_unavailable_timeout, - ): - """Initialize the sensor.""" - self.entity_description = description - self.poller = poller - self.data: list[Any] = [] - if prefix: - self._attr_name = f"{prefix} {description.name}" - self._attr_force_update = force_update - self.go_unavailable_timeout = go_unavailable_timeout - self.last_successful_update = dt_util.utc_from_timestamp(0) - # Median is used to filter out outliers. median of 3 will filter - # single outliers, while median of 5 will filter double outliers - # Use median_count = 1 if no filtering is required. - self.median_count = median - - async def async_added_to_hass(self): - """Set initial state.""" - - @callback - def on_startup(_): - self.async_schedule_update_ha_state(True) - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, on_startup) - - @property - def available(self): - """Return True if did update since 2h.""" - return self.last_successful_update > ( - dt_util.utcnow() - self.go_unavailable_timeout - ) - - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return {ATTR_LAST_SUCCESSFUL_UPDATE: self.last_successful_update} - - def update(self): - """ - Update current conditions. - - This uses a rolling median over 3 values to filter out outliers. - """ - try: - _LOGGER.debug("Polling data for %s", self.name) - data = self.poller.parameter_value(self.entity_description.key) - except (OSError, BluetoothBackendException) as err: - _LOGGER.info("Polling error %s: %s", type(err).__name__, err) - return - - if data is not None: - _LOGGER.debug("%s = %s", self.name, data) - self.data.append(data) - self.last_successful_update = dt_util.utcnow() - else: - _LOGGER.info("Did not receive any data from Mi Flora sensor %s", self.name) - # Remove old data from median list or set sensor value to None - # if no data is available anymore - if self.data: - self.data = self.data[1:] - else: - self._attr_native_value = None - return - - _LOGGER.debug("Data collected: %s", self.data) - if len(self.data) > self.median_count: - self.data = self.data[1:] - - if len(self.data) == self.median_count: - median = sorted(self.data)[int((self.median_count - 1) / 2)] - _LOGGER.debug("Median is: %s", median) - self._attr_native_value = median - elif self._attr_native_value is None: - _LOGGER.debug("Set initial state") - self._attr_native_value = self.data[0] - else: - _LOGGER.debug("Not yet enough data for median calculation") diff --git a/homeassistant/components/miflora/strings.json b/homeassistant/components/miflora/strings.json new file mode 100644 index 00000000000..03427e88af9 --- /dev/null +++ b/homeassistant/components/miflora/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "title": "The Mi Flora integration has been replaced", + "description": "The Mi Flora integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Mi Flora device using the new integration manually.\n\nYour existing Mi Flora YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/miflora/translations/en.json b/homeassistant/components/miflora/translations/en.json new file mode 100644 index 00000000000..52a8a71594c --- /dev/null +++ b/homeassistant/components/miflora/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "The Mi Flora integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Mi Flora device using the new integration manually.\n\nYour existing Mi Flora YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Mi Flora integration has been replaced" + } + } +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 2fa86ae9053..972aae86c85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -420,7 +420,6 @@ blinkstick==1.2.0 blockchain==1.4.4 # homeassistant.components.decora -# homeassistant.components.miflora # homeassistant.components.zengge # bluepy==1.3.0 @@ -1037,9 +1036,6 @@ mficlient==0.3.0 # homeassistant.components.xiaomi_miio micloud==0.5 -# homeassistant.components.miflora -miflora==0.7.2 - # homeassistant.components.mill mill-local==0.1.1 From c63a838b472b9c285e4266689ff89bd492cc3518 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 17:42:22 +0200 Subject: [PATCH 708/820] Raise repair issue for Xiaomi Mijia BLE Temperature and Humidity Sensor (#75754) --- .../components/mitemp_bt/manifest.json | 6 +- homeassistant/components/mitemp_bt/sensor.py | 187 ++---------------- .../components/mitemp_bt/strings.json | 8 + .../components/mitemp_bt/translations/en.json | 8 + requirements_all.txt | 3 - 5 files changed, 33 insertions(+), 179 deletions(-) create mode 100644 homeassistant/components/mitemp_bt/strings.json create mode 100644 homeassistant/components/mitemp_bt/translations/en.json diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 07121b3695b..3af60301802 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -2,8 +2,8 @@ "domain": "mitemp_bt", "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", - "requirements": ["mitemp_bt==0.0.5"], + "requirements": [], + "dependencies": ["repairs"], "codeowners": [], - "iot_class": "local_polling", - "loggers": ["btlewrap", "mitemp_bt"] + "iot_class": "local_polling" } diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index e7f6237fdb1..74d0db7648c 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -1,188 +1,29 @@ """Support for Xiaomi Mi Temp BLE environmental sensor.""" from __future__ import annotations -import logging -from typing import Any - -import btlewrap -from btlewrap.base import BluetoothBackendException -from mitemp_bt import mitemp_bt_poller -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONF_FORCE_UPDATE, - CONF_MAC, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_TIMEOUT, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - - BACKEND = btlewrap.BluepyBackend -except ImportError: - BACKEND = btlewrap.GatttoolBackend - -_LOGGER = logging.getLogger(__name__) - -CONF_ADAPTER = "adapter" -CONF_CACHE = "cache_value" -CONF_MEDIAN = "median" -CONF_RETRIES = "retries" - -DEFAULT_ADAPTER = "hci0" -DEFAULT_UPDATE_INTERVAL = 300 -DEFAULT_FORCE_UPDATE = False -DEFAULT_MEDIAN = 3 -DEFAULT_NAME = "MiTemp BT" -DEFAULT_RETRIES = 2 -DEFAULT_TIMEOUT = 10 +PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="temperature", - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_CELSIUS, - ), - SensorEntityDescription( - key="humidity", - name="Humidity", - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="battery", - name="Battery", - device_class=SensorDeviceClass.BATTERY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ), -) - -SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, - vol.Optional(CONF_CACHE, default=DEFAULT_UPDATE_INTERVAL): cv.positive_int, - vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, - } -) - - -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the MiTempBt sensor.""" - backend = BACKEND - _LOGGER.debug("MiTempBt is using %s backend", backend.__name__) - - cache = config[CONF_CACHE] - poller = mitemp_bt_poller.MiTempBtPoller( - config[CONF_MAC], - cache_timeout=cache, - adapter=config[CONF_ADAPTER], - backend=backend, + async_create_issue( + hass, + "mitemp_bt", + "replaced", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="replaced", + learn_more_url="https://www.home-assistant.io/integrations/xiaomi_ble/", ) - prefix = config[CONF_NAME] - force_update = config[CONF_FORCE_UPDATE] - median = config[CONF_MEDIAN] - poller.ble_timeout = config[CONF_TIMEOUT] - poller.retries = config[CONF_RETRIES] - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - MiTempBtSensor(poller, prefix, force_update, median, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities) - - -class MiTempBtSensor(SensorEntity): - """Implementing the MiTempBt sensor.""" - - def __init__( - self, poller, prefix, force_update, median, description: SensorEntityDescription - ): - """Initialize the sensor.""" - self.entity_description = description - self.poller = poller - self.data: list[Any] = [] - self._attr_name = f"{prefix} {description.name}" - self._attr_force_update = force_update - # Median is used to filter out outliers. median of 3 will filter - # single outliers, while median of 5 will filter double outliers - # Use median_count = 1 if no filtering is required. - self.median_count = median - - def update(self): - """ - Update current conditions. - - This uses a rolling median over 3 values to filter out outliers. - """ - try: - _LOGGER.debug("Polling data for %s", self.name) - data = self.poller.parameter_value(self.entity_description.key) - except OSError as ioerr: - _LOGGER.warning("Polling error %s", ioerr) - return - except BluetoothBackendException as bterror: - _LOGGER.warning("Polling error %s", bterror) - return - - if data is not None: - _LOGGER.debug("%s = %s", self.name, data) - self.data.append(data) - else: - _LOGGER.warning( - "Did not receive any data from Mi Temp sensor %s", self.name - ) - # Remove old data from median list or set sensor value to None - # if no data is available anymore - if self.data: - self.data = self.data[1:] - else: - self._attr_native_value = None - return - - if len(self.data) > self.median_count: - self.data = self.data[1:] - - if len(self.data) == self.median_count: - median = sorted(self.data)[int((self.median_count - 1) / 2)] - _LOGGER.debug("Median is: %s", median) - self._attr_native_value = median - else: - _LOGGER.debug("Not yet enough data for median calculation") diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json new file mode 100644 index 00000000000..78f047896e1 --- /dev/null +++ b/homeassistant/components/mitemp_bt/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/mitemp_bt/translations/en.json b/homeassistant/components/mitemp_bt/translations/en.json new file mode 100644 index 00000000000..ff131abe367 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced" + } + } +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 972aae86c85..61f62f3778c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1045,9 +1045,6 @@ millheater==0.9.0 # homeassistant.components.minio minio==5.0.10 -# homeassistant.components.mitemp_bt -mitemp_bt==0.0.5 - # homeassistant.components.moat moat-ble==0.1.1 From fad7a6cb08ef402456dcdd71d48e7d6f99f96ba2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Jul 2022 07:09:46 -1000 Subject: [PATCH 709/820] Bump sensorpush-ble to 1.5.1 (#75771) --- homeassistant/components/sensorpush/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index 08212f88132..a5d900aaf3b 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -8,7 +8,7 @@ "local_name": "SensorPush*" } ], - "requirements": ["sensorpush-ble==1.4.2"], + "requirements": ["sensorpush-ble==1.5.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 61f62f3778c..8f89fa07f01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2160,7 +2160,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.4.2 +sensorpush-ble==1.5.1 # homeassistant.components.sentry sentry-sdk==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd79723fc79..d87bce47ab5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1451,7 +1451,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.4.2 +sensorpush-ble==1.5.1 # homeassistant.components.sentry sentry-sdk==1.8.0 From cb17a01e48132f7adc2452ce71758512d99a7e1f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 20:01:15 +0200 Subject: [PATCH 710/820] Raise YAML removal issue for nVent RAYCHEM SENZ (#75757) --- homeassistant/components/senz/__init__.py | 18 ++++++++++++++++++ homeassistant/components/senz/manifest.json | 2 +- homeassistant/components/senz/strings.json | 6 ++++++ .../components/senz/translations/en.json | 6 ++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 012b40f5def..9155c8ca036 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -7,6 +7,7 @@ import logging from aiosenz import SENZAPI, Thermostat from httpx import RequestError +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -16,6 +17,7 @@ from homeassistant.helpers import ( config_validation as cv, httpx_client, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .api import SENZConfigEntryAuth @@ -32,6 +34,22 @@ PLATFORMS = [Platform.CLIMATE] SENZDataUpdateCoordinator = DataUpdateCoordinator[dict[str, Thermostat]] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the SENZ integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SENZ from a config entry.""" implementation = ( diff --git a/homeassistant/components/senz/manifest.json b/homeassistant/components/senz/manifest.json index 937a20d8482..36687e46d4a 100644 --- a/homeassistant/components/senz/manifest.json +++ b/homeassistant/components/senz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/senz", "requirements": ["aiosenz==1.0.0"], - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "codeowners": ["@milanmeu"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/senz/strings.json b/homeassistant/components/senz/strings.json index 316f7234f9b..74ca9f5e3bf 100644 --- a/homeassistant/components/senz/strings.json +++ b/homeassistant/components/senz/strings.json @@ -16,5 +16,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "removed_yaml": { + "title": "The nVent RAYCHEM SENZ YAML configuration has been removed", + "description": "Configuring nVent RAYCHEM SENZ using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/senz/translations/en.json b/homeassistant/components/senz/translations/en.json index bdf574691c5..fc1cfd561d4 100644 --- a/homeassistant/components/senz/translations/en.json +++ b/homeassistant/components/senz/translations/en.json @@ -16,5 +16,11 @@ "title": "Pick Authentication Method" } } + }, + "issues": { + "removed_yaml": { + "description": "Configuring nVent RAYCHEM SENZ using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The nVent RAYCHEM SENZ YAML configuration has been removed" + } } } \ No newline at end of file From 157f7292d7df5f2fad60f445b9370551b724585c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 20:02:50 +0200 Subject: [PATCH 711/820] Raise YAML removal issue for Lyric (#75756) --- homeassistant/components/lyric/__init__.py | 18 ++++++++++++++++++ homeassistant/components/lyric/manifest.json | 2 +- homeassistant/components/lyric/strings.json | 6 ++++++ .../components/lyric/translations/en.json | 6 ++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 4e4eec85899..d1048ac8e58 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -12,6 +12,7 @@ from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import async_timeout +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -23,6 +24,7 @@ from homeassistant.helpers import ( device_registry as dr, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -43,6 +45,22 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Honeywell Lyric integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Honeywell Lyric from a config entry.""" implementation = ( diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index c0d9168f46f..91b152cdf21 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Lyric", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "requirements": ["aiolyric==1.0.8"], "codeowners": ["@timmo001"], "quality_scale": "silver", diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 3c9cd6043df..dd9a89f294d 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -17,5 +17,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "removed_yaml": { + "title": "The Honeywell Lyric YAML configuration has been removed", + "description": "Configuring Honeywell Lyric using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json index 17586f16109..3d1df448ba2 100644 --- a/homeassistant/components/lyric/translations/en.json +++ b/homeassistant/components/lyric/translations/en.json @@ -17,5 +17,11 @@ "title": "Reauthenticate Integration" } } + }, + "issues": { + "removed_yaml": { + "description": "Configuring Honeywell Lyric using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Honeywell Lyric YAML configuration has been removed" + } } } \ No newline at end of file From 1e85ddabfd0e59a0106ea8d50f1e6ee5d3f84bb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Jul 2022 09:29:23 -1000 Subject: [PATCH 712/820] Fix startup race in BLE integrations (#75780) --- .../bluetooth/passive_update_coordinator.py | 11 ----------- .../bluetooth/passive_update_processor.py | 11 ----------- .../components/bluetooth/update_coordinator.py | 11 +++++++++++ homeassistant/components/govee_ble/__init__.py | 5 ++++- homeassistant/components/govee_ble/sensor.py | 2 +- homeassistant/components/inkbird/__init__.py | 5 ++++- homeassistant/components/inkbird/sensor.py | 2 +- homeassistant/components/moat/__init__.py | 5 ++++- homeassistant/components/moat/sensor.py | 2 +- .../components/sensorpush/__init__.py | 5 ++++- homeassistant/components/sensorpush/sensor.py | 2 +- .../components/xiaomi_ble/__init__.py | 5 ++++- homeassistant/components/xiaomi_ble/sensor.py | 2 +- .../bluetooth/test_passive_update_processor.py | 18 ++++++++++++++++++ 14 files changed, 54 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 4a22f03449e..97e7ddc49ee 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -42,17 +42,6 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): super()._async_handle_unavailable(address) self.async_update_listeners() - @callback - def async_start(self) -> CALLBACK_TYPE: - """Start the data updater.""" - self._async_start() - - @callback - def _async_cancel() -> None: - self._async_stop() - - return _async_cancel - @callback def async_add_listener( self, update_callback: CALLBACK_TYPE, context: Any = None diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 2e4118000bd..43467701879 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -78,21 +78,10 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): def remove_processor() -> None: """Remove a processor.""" self._processors.remove(processor) - self._async_handle_processors_changed() self._processors.append(processor) - self._async_handle_processors_changed() return remove_processor - @callback - def _async_handle_processors_changed(self) -> None: - """Handle processors changed.""" - running = bool(self._cancel_bluetooth_advertisements) - if running and not self._processors: - self._async_stop() - elif not running and self._processors: - self._async_start() - @callback def _async_handle_unavailable(self, address: str) -> None: """Handle the device going unavailable.""" diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index d45514ab9ab..b1cb2de1453 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -38,6 +38,17 @@ class BasePassiveBluetoothCoordinator: self._present = False self.last_seen = 0.0 + @callback + def async_start(self) -> CALLBACK_TYPE: + """Start the data updater.""" + self._async_start() + + @callback + def _async_cancel() -> None: + self._async_stop() + + return _async_cancel + @property def available(self) -> bool: """Return if the device is available.""" diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index 3cb9e4659d6..3099d401e9b 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Govee BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index b8af7df8fb9..d0b9447d9e3 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -135,12 +135,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( GoveeBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class GoveeBluetoothSensorEntity( diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 574f2a23355..5553b1c6ded 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up INKBIRD BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 6b472edefe9..0648ca80383 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -135,12 +135,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( INKBIRDBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class INKBIRDBluetoothSensorEntity( diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py index bd32ef64a0f..259b6b66709 100644 --- a/homeassistant/components/moat/__init__.py +++ b/homeassistant/components/moat/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Moat BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py index f29111a3406..295e2877aed 100644 --- a/homeassistant/components/moat/sensor.py +++ b/homeassistant/components/moat/sensor.py @@ -142,12 +142,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( MoatBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class MoatBluetoothSensorEntity( diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 0f0d4d4fe8f..0a9efcbc752 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SensorPush BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 3921cbe43dd..9bfa59e3876 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -136,12 +136,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( SensorPushBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class SensorPushBluetoothSensorEntity( diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index a40dc8995d1..4eb20dbd943 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Xiaomi BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 9cdf661f5ad..f33c864fa4a 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -167,12 +167,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( XiaomiBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class XiaomiBluetoothSensorEntity( diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index e5f992eebb2..ebb69d7c7d0 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -107,6 +107,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() entity_key = PassiveBluetoothEntityKey("temperature", None) entity_key_events = [] @@ -171,6 +172,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): assert coordinator.available is True unregister_processor() + cancel_coordinator() async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): @@ -206,6 +208,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() mock_entity = MagicMock() mock_add_entities = MagicMock() @@ -259,6 +262,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): assert processor.available is False unregister_processor() + cancel_coordinator() async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): @@ -290,6 +294,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() all_events = [] @@ -310,6 +315,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 unregister_processor() + cancel_coordinator() async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): @@ -346,6 +352,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_listener(MagicMock()) @@ -361,6 +368,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert processor.available is True unregister_processor() + cancel_coordinator() async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): @@ -397,6 +405,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_listener(MagicMock()) @@ -413,6 +422,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert processor.available is True unregister_processor() + cancel_coordinator() GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( @@ -737,6 +747,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): _async_register_callback, ): coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_listener(MagicMock()) @@ -781,6 +792,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): assert entity_one.entity_key == PassiveBluetoothEntityKey( key="temperature", device_id="remote" ) + cancel_coordinator() NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( @@ -845,6 +857,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner _async_register_callback, ): coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() mock_add_entities = MagicMock() @@ -873,6 +886,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner assert entity_one.entity_key == PassiveBluetoothEntityKey( key="temperature", device_id=None ) + cancel_coordinator() async def test_passive_bluetooth_entity_with_entity_platform( @@ -907,6 +921,7 @@ async def test_passive_bluetooth_entity_with_entity_platform( _async_register_callback, ): coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_entities_listener( PassiveBluetoothProcessorEntity, @@ -926,6 +941,7 @@ async def test_passive_bluetooth_entity_with_entity_platform( hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") is not None ) + cancel_coordinator() SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( @@ -999,6 +1015,7 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st ): coordinator.async_register_processor(binary_sensor_processor) coordinator.async_register_processor(sesnor_processor) + cancel_coordinator = coordinator.async_start() binary_sensor_processor.async_add_listener(MagicMock()) sesnor_processor.async_add_listener(MagicMock()) @@ -1056,3 +1073,4 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( key="motion", device_id=None ) + cancel_coordinator() From bb4ee1ba3273d03b0b9d86dade9c1e145847f1bd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 21:44:12 +0200 Subject: [PATCH 713/820] Adjust wording in raised mitemp_bt issue (#75779) --- homeassistant/components/mitemp_bt/strings.json | 2 +- homeassistant/components/mitemp_bt/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json index 78f047896e1..d36c25eafec 100644 --- a/homeassistant/components/mitemp_bt/strings.json +++ b/homeassistant/components/mitemp_bt/strings.json @@ -2,7 +2,7 @@ "issues": { "replaced": { "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/mitemp_bt/translations/en.json b/homeassistant/components/mitemp_bt/translations/en.json index ff131abe367..cd5113ee02f 100644 --- a/homeassistant/components/mitemp_bt/translations/en.json +++ b/homeassistant/components/mitemp_bt/translations/en.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced" } } From 184e254a43405c6add32f15512f0b4075a5ce98e Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 26 Jul 2022 21:50:41 +0200 Subject: [PATCH 714/820] Bump python-eq3bt requirement (#75145) --- homeassistant/components/eq3btsmart/climate.py | 3 +-- homeassistant/components/eq3btsmart/manifest.json | 7 ++++--- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 1f95f03bd17..412bb8eddeb 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from bluepy.btle import BTLEException # pylint: disable=import-error import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol @@ -218,5 +217,5 @@ class EQ3BTSmartThermostat(ClimateEntity): try: self._thermostat.update() - except BTLEException as ex: + except eq3.BackendException as ex: _LOGGER.warning("Updating the state failed: %s", ex) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 4ad8d08adf5..ade3bc0d912 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -1,9 +1,10 @@ { "domain": "eq3btsmart", - "name": "EQ3 Bluetooth Smart Thermostats", + "name": "eQ-3 Bluetooth Smart Thermostats", "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", - "requirements": ["construct==2.10.56", "python-eq3bt==0.1.11"], + "requirements": ["construct==2.10.56", "python-eq3bt==0.2"], + "dependencies": ["bluetooth"], "codeowners": ["@rytilahti"], "iot_class": "local_polling", - "loggers": ["bluepy", "eq3bt"] + "loggers": ["bleak", "eq3bt"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8f89fa07f01..11503aa8f1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1893,7 +1893,7 @@ python-digitalocean==1.13.2 python-ecobee-api==0.2.14 # homeassistant.components.eq3btsmart -# python-eq3bt==0.1.11 +# python-eq3bt==0.2 # homeassistant.components.etherscan python-etherscan-api==0.0.3 From aaaca0b2bdc9174f1abd9a2cdc85716989f2e971 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 26 Jul 2022 22:02:50 +0200 Subject: [PATCH 715/820] Add tests for the Plugwise Select platform (#75774) --- .coveragerc | 1 - tests/components/plugwise/test_select.py | 44 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/components/plugwise/test_select.py diff --git a/.coveragerc b/.coveragerc index 873629ddd0b..d529cdbd9ca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -927,7 +927,6 @@ omit = homeassistant/components/plex/cast.py homeassistant/components/plex/media_player.py homeassistant/components/plex/view.py - homeassistant/components/plugwise/select.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/__init__.py diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py new file mode 100644 index 00000000000..7ec5559a608 --- /dev/null +++ b/tests/components/plugwise/test_select.py @@ -0,0 +1,44 @@ +"""Tests for the Plugwise Select integration.""" + +from unittest.mock import MagicMock + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_adam_select_entities( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test a select.""" + + state = hass.states.get("select.zone_lisa_wk_thermostat_schedule") + assert state + assert state.state == "GF7 Woonkamer" + + +async def test_adam_change_select_entity( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test changing of select entities.""" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.zone_lisa_wk_thermostat_schedule", + ATTR_OPTION: "Badkamer Schema", + }, + blocking=True, + ) + + assert mock_smile_adam.set_schedule_state.call_count == 1 + mock_smile_adam.set_schedule_state.assert_called_with( + "c50f167537524366a5af7aa3942feb1e", "Badkamer Schema", "on" + ) From 2b1a5e5549700df99b2630a9f3840aac499b6cd7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:19:51 -0400 Subject: [PATCH 716/820] Bump ZHA dependencies (#75785) --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 5d5ce7fef12..179302af1cd 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.1", + "bellows==0.31.2", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.77", + "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.47.3", + "zigpy==0.48.0", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", "zigpy-znp==0.8.1" diff --git a/requirements_all.txt b/requirements_all.txt index 11503aa8f1a..7a8f314df9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.1 +bellows==0.31.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -2514,7 +2514,7 @@ zengge==0.2 zeroconf==0.38.7 # homeassistant.components.zha -zha-quirks==0.0.77 +zha-quirks==0.0.78 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2535,7 +2535,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.3 +zigpy==0.48.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d87bce47ab5..bc952f553db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.1 +bellows==0.31.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -1694,7 +1694,7 @@ youless-api==0.16 zeroconf==0.38.7 # homeassistant.components.zha -zha-quirks==0.0.77 +zha-quirks==0.0.78 # homeassistant.components.zha zigpy-deconz==0.18.0 @@ -1709,7 +1709,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.3 +zigpy==0.48.0 # homeassistant.components.zwave_js zwave-js-server-python==0.39.0 From e2dd2c9424ebbe61559d2bfe29ff53ed651dca52 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 26 Jul 2022 16:42:08 -0400 Subject: [PATCH 717/820] Add guards to ZHA light groups when using hs color mode (#75599) * fix polling currentSaturation * Guard light groups against enhanced current hue * Use XY if all group members do not support HS * add config option to always prefer XY color mode * use correct enum * remove periods --- homeassistant/components/zha/core/const.py | 2 + homeassistant/components/zha/light.py | 58 +++++++++++++++---- homeassistant/components/zha/strings.json | 1 + .../components/zha/translations/en.json | 1 + 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index acac96f3162..c72b6373ad0 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -129,6 +129,7 @@ CONF_DATABASE = "database_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" +CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_FLOWCONTROL = "flow_control" @@ -145,6 +146,7 @@ CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, + vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Optional( CONF_CONSIDER_UNAVAILABLE_MAINS, diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 190e0b2319d..23552c0f469 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -40,6 +40,7 @@ from .core.const import ( CHANNEL_COLOR, CHANNEL_LEVEL, CHANNEL_ON_OFF, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, CONF_DEFAULT_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, DATA_ZHA, @@ -120,6 +121,7 @@ class BaseLight(LogMixin, light.LightEntity): self._off_brightness: int | None = None self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._zha_config_enhanced_light_transition: bool = False + self._zha_config_always_prefer_xy_color_mode: bool = True self._on_off_channel = None self._level_channel = None self._color_channel = None @@ -280,7 +282,10 @@ class BaseLight(LogMixin, light.LightEntity): self._attr_hs_color = None if hs_color is not None: - if self._color_channel.enhanced_hue_supported: + if ( + not isinstance(self, LightGroup) + and self._color_channel.enhanced_hue_supported + ): result = await self._color_channel.enhanced_move_to_hue_and_saturation( int(hs_color[0] * 65535 / 360), int(hs_color[1] * 2.54), @@ -418,6 +423,13 @@ class Light(BaseLight, ZhaEntity): self._cancel_refresh_handle = None effect_list = [] + self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + True, + ) + self._attr_supported_color_modes = {ColorMode.ONOFF} if self._level_channel: self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) @@ -429,9 +441,9 @@ class Light(BaseLight, ZhaEntity): self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) self._attr_color_temp = self._color_channel.color_temperature - if ( - self._color_channel.xy_supported - and not self._color_channel.hs_supported + if self._color_channel.xy_supported and ( + self._zha_config_always_prefer_xy_color_mode + or not self._color_channel.hs_supported ): self._attr_supported_color_modes.add(ColorMode.XY) curr_x = self._color_channel.current_x @@ -441,7 +453,10 @@ class Light(BaseLight, ZhaEntity): else: self._attr_xy_color = (0, 0) - if self._color_channel.hs_supported: + if ( + self._color_channel.hs_supported + and not self._zha_config_always_prefer_xy_color_mode + ): self._attr_supported_color_modes.add(ColorMode.HS) if self._color_channel.enhanced_hue_supported: curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 @@ -572,12 +587,16 @@ class Light(BaseLight, ZhaEntity): "current_x", "current_y", ] - if self._color_channel.enhanced_hue_supported: + if ( + not self._zha_config_always_prefer_xy_color_mode + and self._color_channel.enhanced_hue_supported + ): attributes.append("enhanced_current_hue") attributes.append("current_saturation") if ( self._color_channel.hs_supported and not self._color_channel.enhanced_hue_supported + and not self._zha_config_always_prefer_xy_color_mode ): attributes.append("current_hue") attributes.append("current_saturation") @@ -598,7 +617,10 @@ class Light(BaseLight, ZhaEntity): self._attr_color_temp = color_temp self._attr_xy_color = None self._attr_hs_color = None - elif color_mode == Color.ColorMode.Hue_and_saturation: + elif ( + color_mode == Color.ColorMode.Hue_and_saturation + and not self._zha_config_always_prefer_xy_color_mode + ): self._attr_color_mode = ColorMode.HS if self._color_channel.enhanced_hue_supported: current_hue = results.get("enhanced_current_hue") @@ -610,12 +632,12 @@ class Light(BaseLight, ZhaEntity): int(current_hue * 360 / 65535) if self._color_channel.enhanced_hue_supported else int(current_hue * 360 / 254), - int(current_saturation / 254), + int(current_saturation / 2.54), ) self._attr_xy_color = None self._attr_color_temp = None else: - self._attr_color_mode = Color.ColorMode.X_and_Y + self._attr_color_mode = ColorMode.XY color_x = results.get("current_x") color_y = results.get("current_y") if color_x is not None and color_y is not None: @@ -704,6 +726,12 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + True, + ) self._zha_config_enhanced_light_transition = False self._attr_color_mode = None @@ -753,9 +781,10 @@ class LightGroup(BaseLight, ZhaGroupEntity): on_states, light.ATTR_XY_COLOR, reduce=helpers.mean_tuple ) - self._attr_hs_color = helpers.reduce_attribute( - on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple - ) + if not self._zha_config_always_prefer_xy_color_mode: + self._attr_hs_color = helpers.reduce_attribute( + on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple + ) self._attr_color_temp = helpers.reduce_attribute( on_states, light.ATTR_COLOR_TEMP @@ -794,6 +823,11 @@ class LightGroup(BaseLight, ZhaGroupEntity): if ColorMode.BRIGHTNESS in color_mode_count: color_mode_count[ColorMode.BRIGHTNESS] = 0 self._attr_color_mode = color_mode_count.most_common(1)[0][0] + if self._attr_color_mode == ColorMode.HS and ( + color_mode_count[ColorMode.HS] != len(self._group.members) + or self._zha_config_always_prefer_xy_color_mode + ): # switch to XY if all members do not support HS + self._attr_color_mode = ColorMode.XY self._attr_supported_color_modes = None all_supported_color_modes = list( diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index eb7753276f8..f5321565be0 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -38,6 +38,7 @@ "zha_options": { "title": "Global Options", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", + "always_prefer_xy_color_mode": "Always prefer XY color mode", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 91c23acc044..0a7795a47b4 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -51,6 +51,7 @@ "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", + "always_prefer_xy_color_mode": "Always prefer XY color mode", "title": "Global Options" } }, From 0ff34f232c931367d5c221170bd2c31de65d07a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 22:42:19 +0200 Subject: [PATCH 718/820] Add events to repairs issue registry changes (#75784) --- .../components/repairs/issue_registry.py | 17 ++++++ .../components/repairs/test_issue_registry.py | 59 ++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index c1eda0d53be..5c459309cc0 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -13,6 +13,7 @@ import homeassistant.util.dt as dt_util from .models import IssueSeverity DATA_REGISTRY = "issue_registry" +EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED = "repairs_issue_registry_updated" STORAGE_KEY = "repairs.issue_registry" STORAGE_VERSION = 1 SAVE_DELAY = 10 @@ -82,6 +83,10 @@ class IssueRegistry: ) self.issues[(domain, issue_id)] = issue self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "create", "domain": domain, "issue_id": issue_id}, + ) else: issue = self.issues[(domain, issue_id)] = dataclasses.replace( issue, @@ -93,6 +98,10 @@ class IssueRegistry: translation_key=translation_key, translation_placeholders=translation_placeholders, ) + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "update", "domain": domain, "issue_id": issue_id}, + ) return issue @@ -103,6 +112,10 @@ class IssueRegistry: return self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "remove", "domain": domain, "issue_id": issue_id}, + ) @callback def async_ignore(self, domain: str, issue_id: str, ignore: bool) -> IssueEntry: @@ -118,6 +131,10 @@ class IssueRegistry: ) self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "update", "domain": domain, "issue_id": issue_id}, + ) return issue diff --git a/tests/components/repairs/test_issue_registry.py b/tests/components/repairs/test_issue_registry.py index 1af67601581..523f75bfdc2 100644 --- a/tests/components/repairs/test_issue_registry.py +++ b/tests/components/repairs/test_issue_registry.py @@ -1,11 +1,14 @@ """Test the repairs websocket API.""" from homeassistant.components.repairs import async_create_issue, issue_registry from homeassistant.components.repairs.const import DOMAIN -from homeassistant.components.repairs.issue_handler import async_ignore_issue +from homeassistant.components.repairs.issue_handler import ( + async_delete_issue, + async_ignore_issue, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import flush_store +from tests.common import async_capture_events, flush_store async def test_load_issues(hass: HomeAssistant) -> None: @@ -33,8 +36,22 @@ async def test_load_issues(hass: HomeAssistant) -> None: "translation_key": "even_worse", "translation_placeholders": {"def": "456"}, }, + { + "breaks_in_ha_version": "2022.7", + "domain": "test", + "issue_id": "issue_3", + "is_fixable": True, + "learn_more_url": "https://checkboxrace.com", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "789"}, + }, ] + events = async_capture_events( + hass, issue_registry.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED + ) + for issue in issues: async_create_issue( hass, @@ -47,7 +64,45 @@ async def test_load_issues(hass: HomeAssistant) -> None: translation_key=issue["translation_key"], translation_placeholders=issue["translation_placeholders"], ) + + await hass.async_block_till_done() + + assert len(events) == 3 + assert events[0].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_1", + } + assert events[1].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_2", + } + assert events[2].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_3", + } + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) + await hass.async_block_till_done() + + assert len(events) == 4 + assert events[3].data == { + "action": "update", + "domain": "test", + "issue_id": "issue_1", + } + + async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) + await hass.async_block_till_done() + + assert len(events) == 5 + assert events[4].data == { + "action": "remove", + "domain": "test", + "issue_id": "issue_3", + } registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] assert len(registry.issues) == 2 From c5912f0faee0936b972bbe994bf45018dabb793e Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 27 Jul 2022 05:04:08 +0800 Subject: [PATCH 719/820] Cleanup unused camera constants (#75772) Cleanup unused camera constants after #75452 --- homeassistant/components/camera/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index dcee6c6c5ed..247f73c89f2 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -129,14 +129,6 @@ CAMERA_SERVICE_RECORD: Final = { vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), } -WS_TYPE_CAMERA_THUMBNAIL: Final = "camera_thumbnail" -SCHEMA_WS_CAMERA_THUMBNAIL: Final = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_CAMERA_THUMBNAIL, - vol.Required("entity_id"): cv.entity_id, - } -) - @dataclass class CameraEntityDescription(EntityDescription): From fb52f5098fd84bff7753628907926de2661d16ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Henriques?= Date: Tue, 26 Jul 2022 22:36:46 +0100 Subject: [PATCH 720/820] Add en-GB locale for AlexaMotionSensor and AlexaContactSensor (#75705) Added en-GB as supported locale for AlexaMotionSensor and AlexaContactSensor --- homeassistant/components/alexa/capabilities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 25ec43b689c..3963372fec1 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -903,6 +903,7 @@ class AlexaContactSensor(AlexaCapability): "en-CA", "en-IN", "en-US", + "en-GB", "es-ES", "it-IT", "ja-JP", @@ -951,6 +952,7 @@ class AlexaMotionSensor(AlexaCapability): "en-CA", "en-IN", "en-US", + "en-GB", "es-ES", "it-IT", "ja-JP", From 6868865e3ebc303e6e3d76d669f5ba31fad32b14 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 27 Jul 2022 00:01:49 +0200 Subject: [PATCH 721/820] Add 1.5 second sleep to motion blinds update (#75494) --- .../components/motion_blinds/__init__.py | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 184e721aeed..dfbc6ab74a7 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -65,36 +65,43 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): self._wait_for_push = coordinator_info[CONF_WAIT_FOR_PUSH] def update_gateway(self): - """Call all updates using one async_add_executor_job.""" - data = {} - + """Fetch data from gateway.""" try: self._gateway.Update() except (timeout, ParseException): # let the error be logged and handled by the motionblinds library - data[KEY_GATEWAY] = {ATTR_AVAILABLE: False} - return data + return {ATTR_AVAILABLE: False} else: - data[KEY_GATEWAY] = {ATTR_AVAILABLE: True} + return {ATTR_AVAILABLE: True} - for blind in self._gateway.device_list.values(): - try: - if self._wait_for_push: - blind.Update() - else: - blind.Update_trigger() - except (timeout, ParseException): - # let the error be logged and handled by the motionblinds library - data[blind.mac] = {ATTR_AVAILABLE: False} + def update_blind(self, blind): + """Fetch data from a blind.""" + try: + if self._wait_for_push: + blind.Update() else: - data[blind.mac] = {ATTR_AVAILABLE: True} - - return data + blind.Update_trigger() + except (timeout, ParseException): + # let the error be logged and handled by the motionblinds library + return {ATTR_AVAILABLE: False} + else: + return {ATTR_AVAILABLE: True} async def _async_update_data(self): """Fetch the latest data from the gateway and blinds.""" + data = {} + async with self.api_lock: - data = await self.hass.async_add_executor_job(self.update_gateway) + data[KEY_GATEWAY] = await self.hass.async_add_executor_job( + self.update_gateway + ) + + for blind in self._gateway.device_list.values(): + await asyncio.sleep(1.5) + async with self.api_lock: + data[blind.mac] = await self.hass.async_add_executor_job( + self.update_blind, blind + ) all_available = all(device[ATTR_AVAILABLE] for device in data.values()) if all_available: From 129b42cd23301fb4df30a48c971af1200b4c6e00 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 27 Jul 2022 02:03:17 +0200 Subject: [PATCH 722/820] Fix ZHA light brightness jumping around during transitions (#74849) * Moved color commands to a new ``async_handle_color_commands`` method * Fixed tests * Fix brightness jumping around during transitions * Add config option to disable "Enhanced brightness slider during light transition" --- homeassistant/components/zha/core/const.py | 2 + homeassistant/components/zha/light.py | 355 ++++++++++++++---- homeassistant/components/zha/strings.json | 1 + .../components/zha/translations/en.json | 3 +- tests/components/zha/common.py | 11 + tests/components/zha/test_light.py | 14 + 6 files changed, 320 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c72b6373ad0..4c8c6e03c79 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -129,6 +129,7 @@ CONF_DATABASE = "database_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" +CONF_ENABLE_LIGHT_TRANSITIONING_FLAG = "light_transitioning_flag" CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" @@ -146,6 +147,7 @@ CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, + vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean, vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Optional( diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 23552c0f469..9fc089e2241 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,7 +1,9 @@ """Lights on Zigbee Home Automation networks.""" from __future__ import annotations +import asyncio from collections import Counter +from collections.abc import Callable from datetime import timedelta import functools import itertools @@ -26,14 +28,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import async_call_later, async_track_time_interval from .core import discovery, helpers from .core.const import ( @@ -43,6 +45,7 @@ from .core.const import ( CONF_ALWAYS_PREFER_XY_COLOR_MODE, CONF_DEFAULT_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, DATA_ZHA, EFFECT_BLINK, EFFECT_BREATHE, @@ -61,6 +64,10 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +DEFAULT_ON_OFF_TRANSITION = 1 # most bulbs default to a 1-second turn on/off transition +DEFAULT_EXTRA_TRANSITION_DELAY_SHORT = 0.25 +DEFAULT_EXTRA_TRANSITION_DELAY_LONG = 2.0 +DEFAULT_LONG_TRANSITION_TIME = 10 DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 @@ -75,6 +82,8 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" +SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start" +SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished" DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} @@ -111,6 +120,7 @@ class BaseLight(LogMixin, light.LightEntity): def __init__(self, *args, **kwargs): """Initialize the light.""" + self._zha_device: ZHADevice = None super().__init__(*args, **kwargs) self._attr_min_mireds: int | None = 153 self._attr_max_mireds: int | None = 500 @@ -121,11 +131,14 @@ class BaseLight(LogMixin, light.LightEntity): self._off_brightness: int | None = None self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._zha_config_enhanced_light_transition: bool = False + self._zha_config_enable_light_transitioning_flag: bool = True self._zha_config_always_prefer_xy_color_mode: bool = True self._on_off_channel = None self._level_channel = None self._color_channel = None self._identify_channel = None + self._transitioning: bool = False + self._transition_listener: Callable[[], None] | None = None @property def extra_state_attributes(self) -> dict[str, Any]: @@ -151,6 +164,12 @@ class BaseLight(LogMixin, light.LightEntity): on at `on_level` Zigbee attribute value, regardless of the last set level """ + if self._transitioning: + self.debug( + "received level %s while transitioning - skipping update", + value, + ) + return value = max(0, min(254, value)) self._attr_brightness = value self.async_write_ha_state() @@ -170,6 +189,36 @@ class BaseLight(LogMixin, light.LightEntity): xy_color = kwargs.get(light.ATTR_XY_COLOR) hs_color = kwargs.get(light.ATTR_HS_COLOR) + set_transition_flag = ( + brightness_supported(self._attr_supported_color_modes) + or temperature is not None + or xy_color is not None + or hs_color is not None + ) and self._zha_config_enable_light_transitioning_flag + transition_time = ( + ( + duration / 10 + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + if ( + (brightness is not None or transition is not None) + and brightness_supported(self._attr_supported_color_modes) + or (self._off_with_transition and self._off_brightness is not None) + or temperature is not None + or xy_color is not None + or hs_color is not None + ) + else DEFAULT_ON_OFF_TRANSITION + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + ) + if set_transition_flag + else 0 + ) + + # If we need to pause attribute report parsing, we'll do so here. + # After successful calls, we later start a timer to unset the flag after transition_time. + # On an error on the first move to level call, we unset the flag immediately if no previous timer is running. + # On an error on subsequent calls, we start the transition timer, as a brightness call might have come through. + if set_transition_flag: + self.async_transition_set_flag() + # If the light is currently off but a turn_on call with a color/temperature is sent, # the light needs to be turned on first at a low brightness level where the light is immediately transitioned # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness. @@ -230,6 +279,10 @@ class BaseLight(LogMixin, light.LightEntity): ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + # First 'move to level' call failed, so if the transitioning delay isn't running from a previous call, + # the flag can be unset immediately + if set_transition_flag and not self._transition_listener: + self.async_transition_complete() self.debug("turned on: %s", t_log) return # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call @@ -245,6 +298,10 @@ class BaseLight(LogMixin, light.LightEntity): ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + # First 'move to level' call failed, so if the transitioning delay isn't running from a previous call, + # the flag can be unset immediately + if set_transition_flag and not self._transition_listener: + self.async_transition_complete() self.debug("turned on: %s", t_log) return self._attr_state = bool(level) @@ -261,73 +318,25 @@ class BaseLight(LogMixin, light.LightEntity): result = await self._on_off_channel.on() t_log["on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + # 'On' call failed, but as brightness may still transition (for FORCE_ON lights), + # we start the timer to unset the flag after the transition_time if necessary. + self.async_transition_start_timer(transition_time) self.debug("turned on: %s", t_log) return self._attr_state = True - if temperature is not None: - result = await self._color_channel.move_to_color_temp( - temperature, - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["move_to_color_temp"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - self.debug("turned on: %s", t_log) - return - self._attr_color_mode = ColorMode.COLOR_TEMP - self._attr_color_temp = temperature - self._attr_xy_color = None - self._attr_hs_color = None - - if hs_color is not None: - if ( - not isinstance(self, LightGroup) - and self._color_channel.enhanced_hue_supported - ): - result = await self._color_channel.enhanced_move_to_hue_and_saturation( - int(hs_color[0] * 65535 / 360), - int(hs_color[1] * 2.54), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["enhanced_move_to_hue_and_saturation"] = result - else: - result = await self._color_channel.move_to_hue_and_saturation( - int(hs_color[0] * 254 / 360), - int(hs_color[1] * 2.54), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["move_to_hue_and_saturation"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - self.debug("turned on: %s", t_log) - return - self._attr_color_mode = ColorMode.HS - self._attr_hs_color = hs_color - self._attr_xy_color = None - self._attr_color_temp = None - xy_color = None # don't set xy_color if it is also present - - if xy_color is not None: - result = await self._color_channel.move_to_color( - int(xy_color[0] * 65535), - int(xy_color[1] * 65535), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["move_to_color"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - self.debug("turned on: %s", t_log) - return - self._attr_color_mode = ColorMode.XY - self._attr_xy_color = xy_color - self._attr_color_temp = None - self._attr_hs_color = None + if not await self.async_handle_color_commands( + temperature, + duration, + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + # Color calls failed, but as brightness may still transition, we start the timer to unset the flag + self.async_transition_start_timer(transition_time) + self.debug("turned on: %s", t_log) + return if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. @@ -340,6 +349,10 @@ class BaseLight(LogMixin, light.LightEntity): if level: self._attr_brightness = level + # Our light is guaranteed to have just started the transitioning process if necessary, + # so we start the delay for the transition (to stop parsing attribute reports after the completed transition). + self.async_transition_start_timer(transition_time) + if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION @@ -382,6 +395,15 @@ class BaseLight(LogMixin, light.LightEntity): transition = kwargs.get(light.ATTR_TRANSITION) supports_level = brightness_supported(self._attr_supported_color_modes) + transition_time = ( + transition or self._DEFAULT_MIN_TRANSITION_TIME + if transition is not None + else DEFAULT_ON_OFF_TRANSITION + ) + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + # Start pausing attribute report parsing + if self._zha_config_enable_light_transitioning_flag: + self.async_transition_set_flag() + # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( @@ -389,6 +411,10 @@ class BaseLight(LogMixin, light.LightEntity): ) else: result = await self._on_off_channel.off() + + # Pause parsing attribute reports until transition is complete + if self._zha_config_enable_light_transitioning_flag: + self.async_transition_start_timer(transition_time) self.debug("turned off: %s", result) if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return @@ -401,6 +427,127 @@ class BaseLight(LogMixin, light.LightEntity): self.async_write_ha_state() + async def async_handle_color_commands( + self, + temperature, + duration, + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + """Process ZCL color commands.""" + if temperature is not None: + result = await self._color_channel.move_to_color_temp( + temperature, + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_color_temp"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self._attr_color_mode = ColorMode.COLOR_TEMP + self._attr_color_temp = temperature + self._attr_xy_color = None + self._attr_hs_color = None + + if hs_color is not None: + if ( + not isinstance(self, LightGroup) + and self._color_channel.enhanced_hue_supported + ): + result = await self._color_channel.enhanced_move_to_hue_and_saturation( + int(hs_color[0] * 65535 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["enhanced_move_to_hue_and_saturation"] = result + else: + result = await self._color_channel.move_to_hue_and_saturation( + int(hs_color[0] * 254 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_hue_and_saturation"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = hs_color + self._attr_xy_color = None + self._attr_color_temp = None + xy_color = None # don't set xy_color if it is also present + + if xy_color is not None: + result = await self._color_channel.move_to_color( + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_color"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = xy_color + self._attr_color_temp = None + self._attr_hs_color = None + + return True + + @callback + def async_transition_set_flag(self) -> None: + """Set _transitioning to True.""" + self.debug("setting transitioning flag to True") + self._transitioning = True + if isinstance(self, LightGroup): + async_dispatcher_send( + self.hass, + SIGNAL_LIGHT_GROUP_TRANSITION_START, + {"entity_ids": self._entity_ids}, + ) + if self._transition_listener is not None: + self._transition_listener() + + @callback + def async_transition_start_timer(self, transition_time) -> None: + """Start a timer to unset _transitioning after transition_time if necessary.""" + if not transition_time: + return + # For longer transitions, we want to extend the timer a bit more + if transition_time >= DEFAULT_LONG_TRANSITION_TIME: + transition_time += DEFAULT_EXTRA_TRANSITION_DELAY_LONG + self.debug("starting transitioning timer for %s", transition_time) + self._transition_listener = async_call_later( + self._zha_device.hass, + transition_time, + self.async_transition_complete, + ) + + @callback + def async_transition_complete(self, _=None) -> None: + """Set _transitioning to False and write HA state.""" + self.debug("transition complete - future attribute reports will write HA state") + self._transitioning = False + if self._transition_listener: + self._transition_listener() + self._transition_listener = None + self.async_write_ha_state() + if isinstance(self, LightGroup): + async_dispatcher_send( + self.hass, + SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED, + {"entity_ids": self._entity_ids}, + ) + if self._debounced_member_refresh is not None: + self.debug("transition complete - refreshing group member states") + asyncio.create_task(self._debounced_member_refresh.async_call()) + @STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}) class Light(BaseLight, ZhaEntity): @@ -506,10 +653,22 @@ class Light(BaseLight, ZhaEntity): CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, False, ) + self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, + True, + ) @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" + if self._transitioning: + self.debug( + "received onoff %s while transitioning - skipping update", + value, + ) + return self._attr_state = bool(value) if value: self._off_with_transition = False @@ -537,6 +696,38 @@ class Light(BaseLight, ZhaEntity): signal_override=True, ) + @callback + def transition_on(signal): + """Handle a transition start event from a group.""" + if self.entity_id in signal["entity_ids"]: + self.debug( + "group transition started - setting member transitioning flag" + ) + self._transitioning = True + + self.async_accept_signal( + None, + SIGNAL_LIGHT_GROUP_TRANSITION_START, + transition_on, + signal_override=True, + ) + + @callback + def transition_off(signal): + """Handle a transition finished event from a group.""" + if self.entity_id in signal["entity_ids"]: + self.debug( + "group transition completed - unsetting member transitioning flag" + ) + self._transitioning = False + + self.async_accept_signal( + None, + SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED, + transition_off, + signal_override=True, + ) + async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" assert self._cancel_refresh_handle @@ -654,16 +845,25 @@ class Light(BaseLight, ZhaEntity): async def async_update(self): """Update to the latest state.""" + if self._transitioning: + self.debug("skipping async_update while transitioning") + return await self.async_get_state() async def _refresh(self, time): """Call async_get_state at an interval.""" + if self._transitioning: + self.debug("skipping _refresh while transitioning") + return await self.async_get_state() self.async_write_ha_state() async def _maybe_force_refresh(self, signal): """Force update the state if the signal contains the entity id for this entity.""" if self.entity_id in signal["entity_ids"]: + if self._transitioning: + self.debug("skipping _maybe_force_refresh while transitioning") + return await self.async_get_state() self.async_write_ha_state() @@ -726,6 +926,12 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, + True, + ) self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, @@ -757,13 +963,32 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" await super().async_turn_on(**kwargs) + if self._transitioning: + return await self._debounced_member_refresh.async_call() async def async_turn_off(self, **kwargs): """Turn the entity off.""" await super().async_turn_off(**kwargs) + if self._transitioning: + return await self._debounced_member_refresh.async_call() + @callback + def async_state_changed_listener(self, event: Event): + """Handle child updates.""" + if self._transitioning: + self.debug("skipping group entity state update during transition") + return + super().async_state_changed_listener(event) + + async def async_update_ha_state(self, force_refresh: bool = False) -> None: + """Update Home Assistant with current state of entity.""" + if self._transitioning: + self.debug("skipping group entity state update during transition") + return + await super().async_update_ha_state(force_refresh) + async def async_update(self) -> None: """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index f5321565be0..4eb872f4fae 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -38,6 +38,7 @@ "zha_options": { "title": "Global Options", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", + "light_transitioning_flag": "Enable enhanced brightness slider during light transition", "always_prefer_xy_color_mode": "Always prefer XY color mode", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 0a7795a47b4..757ab338ec6 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -46,12 +46,13 @@ "title": "Alarm Control Panel Options" }, "zha_options": { + "always_prefer_xy_color_mode": "Always prefer XY color mode", "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", - "always_prefer_xy_color_mode": "Always prefer XY color mode", + "light_transitioning_flag": "Enable enhanced brightness slider during light transition", "title": "Global Options" } }, diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 6a51f441a78..cad8020267f 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,5 +1,6 @@ """Common test objects.""" import asyncio +from datetime import timedelta import math from unittest.mock import AsyncMock, Mock @@ -8,6 +9,9 @@ import zigpy.zcl.foundation as zcl_f import homeassistant.components.zha.core.const as zha_const from homeassistant.helpers import entity_registry +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed def patch_cluster(cluster): @@ -232,3 +236,10 @@ async def async_wait_for_updates(hass): await asyncio.sleep(0) await asyncio.sleep(0) await hass.async_block_till_done() + + +async def async_shift_time(hass): + """Shift time to cause call later tasks to run.""" + next_update = dt_util.utcnow() + timedelta(seconds=11) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 94f0c96c38d..156f692aa14 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -22,6 +22,7 @@ import homeassistant.util.dt as dt_util from .common import ( async_enable_traffic, async_find_group_entity_id, + async_shift_time, async_test_rejoin, find_entity_id, get_zha_gateway, @@ -346,6 +347,7 @@ async def test_light( await async_test_level_on_off_from_hass( hass, cluster_on_off, cluster_level, entity_id ) + await async_shift_time(hass) # test getting a brightness change from the network await async_test_on_from_light(hass, cluster_on_off, entity_id) @@ -1190,6 +1192,8 @@ async def async_test_level_on_off_from_hass( on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() + await async_shift_time(hass) + # turn on via UI await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True @@ -1210,6 +1214,8 @@ async def async_test_level_on_off_from_hass( on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() + await async_shift_time(hass) + await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -1407,11 +1413,15 @@ async def test_zha_group_light_entity( # test turning the lights on and off from the HA await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) + await async_shift_time(hass) + # test short flashing the lights from the HA await async_test_flash_from_hass( hass, group_cluster_identify, group_entity_id, FLASH_SHORT ) + await async_shift_time(hass) + # test turning the lights on and off from the light await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id) @@ -1424,6 +1434,8 @@ async def test_zha_group_light_entity( expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition ) + await async_shift_time(hass) + # test getting a brightness change from the network await async_test_on_from_light(hass, dev1_cluster_on_off, group_entity_id) await async_test_dimmer_from_light( @@ -1443,6 +1455,8 @@ async def test_zha_group_light_entity( hass, group_cluster_identify, group_entity_id, FLASH_LONG ) + await async_shift_time(hass) + assert len(zha_group.members) == 2 # test some of the group logic to make sure we key off states correctly await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) From 33c635809c1de156845d9a10b5fd364a53b3efb5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 27 Jul 2022 00:28:57 +0000 Subject: [PATCH 723/820] [ci skip] Translation update --- .../components/bluetooth/translations/el.json | 12 ++++++++++- .../components/bluetooth/translations/it.json | 12 ++++++++++- .../components/bluetooth/translations/pl.json | 12 ++++++++++- .../components/bluetooth/translations/ru.json | 12 ++++++++++- .../components/demo/translations/ru.json | 8 ++++++- .../components/esphome/translations/ru.json | 4 ++-- .../components/google/translations/de.json | 10 +++++++++ .../components/google/translations/el.json | 10 +++++++++ .../components/google/translations/en.json | 4 ++-- .../components/google/translations/it.json | 10 +++++++++ .../components/google/translations/pl.json | 10 +++++++++ .../components/google/translations/pt-BR.json | 10 +++++++++ .../components/google/translations/ru.json | 10 +++++++++ .../google/translations/zh-Hant.json | 10 +++++++++ .../components/govee_ble/translations/pl.json | 21 +++++++++++++++++++ .../components/govee_ble/translations/ru.json | 21 +++++++++++++++++++ .../homekit_controller/translations/ru.json | 4 ++-- .../components/knx/translations/ru.json | 2 +- .../components/life360/translations/ru.json | 2 +- .../components/lyric/translations/el.json | 6 ++++++ .../components/lyric/translations/it.json | 6 ++++++ .../components/lyric/translations/pl.json | 6 ++++++ .../components/miflora/translations/de.json | 8 +++++++ .../components/miflora/translations/el.json | 8 +++++++ .../components/miflora/translations/it.json | 8 +++++++ .../components/miflora/translations/pl.json | 8 +++++++ .../miflora/translations/pt-BR.json | 8 +++++++ .../miflora/translations/zh-Hant.json | 8 +++++++ .../components/mitemp_bt/translations/de.json | 8 +++++++ .../components/mitemp_bt/translations/el.json | 8 +++++++ .../components/mitemp_bt/translations/pl.json | 8 +++++++ .../mitemp_bt/translations/pt-BR.json | 8 +++++++ .../mitemp_bt/translations/zh-Hant.json | 8 +++++++ .../components/moat/translations/pl.json | 21 +++++++++++++++++++ .../components/moat/translations/ru.json | 21 +++++++++++++++++++ .../components/nest/translations/ru.json | 14 ++++++++++++- .../components/plugwise/translations/ru.json | 4 ++-- .../radiotherm/translations/el.json | 6 ++++++ .../radiotherm/translations/it.json | 5 +++++ .../radiotherm/translations/pl.json | 6 ++++++ .../radiotherm/translations/ru.json | 6 ++++++ .../components/senz/translations/el.json | 6 ++++++ .../components/senz/translations/it.json | 6 ++++++ .../components/senz/translations/pl.json | 6 ++++++ .../simplisafe/translations/el.json | 5 ++++- .../simplisafe/translations/pl.json | 7 +++++-- .../simplisafe/translations/ru.json | 7 +++++-- .../components/spotify/translations/de.json | 6 ++++++ .../components/spotify/translations/el.json | 6 ++++++ .../components/spotify/translations/it.json | 6 ++++++ .../components/spotify/translations/pl.json | 6 ++++++ .../spotify/translations/pt-BR.json | 6 ++++++ .../components/spotify/translations/ru.json | 6 ++++++ .../spotify/translations/zh-Hant.json | 6 ++++++ .../steam_online/translations/de.json | 6 ++++++ .../steam_online/translations/el.json | 6 ++++++ .../steam_online/translations/it.json | 6 ++++++ .../steam_online/translations/pl.json | 6 ++++++ .../steam_online/translations/pt-BR.json | 6 ++++++ .../steam_online/translations/ru.json | 6 ++++++ .../steam_online/translations/zh-Hant.json | 6 ++++++ .../components/switchbot/translations/el.json | 1 + .../components/switchbot/translations/pl.json | 3 ++- .../components/switchbot/translations/ru.json | 3 ++- .../xiaomi_ble/translations/el.json | 15 +++++++++++++ .../xiaomi_ble/translations/pl.json | 15 +++++++++++++ .../xiaomi_ble/translations/ru.json | 15 +++++++++++++ .../components/zha/translations/it.json | 1 + 68 files changed, 524 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/govee_ble/translations/pl.json create mode 100644 homeassistant/components/govee_ble/translations/ru.json create mode 100644 homeassistant/components/miflora/translations/de.json create mode 100644 homeassistant/components/miflora/translations/el.json create mode 100644 homeassistant/components/miflora/translations/it.json create mode 100644 homeassistant/components/miflora/translations/pl.json create mode 100644 homeassistant/components/miflora/translations/pt-BR.json create mode 100644 homeassistant/components/miflora/translations/zh-Hant.json create mode 100644 homeassistant/components/mitemp_bt/translations/de.json create mode 100644 homeassistant/components/mitemp_bt/translations/el.json create mode 100644 homeassistant/components/mitemp_bt/translations/pl.json create mode 100644 homeassistant/components/mitemp_bt/translations/pt-BR.json create mode 100644 homeassistant/components/mitemp_bt/translations/zh-Hant.json create mode 100644 homeassistant/components/moat/translations/pl.json create mode 100644 homeassistant/components/moat/translations/ru.json diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 03b3be612a3..5a0aee96322 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "no_adapters": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03b5\u03af\u03c2 Bluetooth" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u039f \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2 Bluetooth \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index af83bfd271d..86809b41a7d 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "no_adapters": "Nessun adattatore Bluetooth trovato" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Seleziona un dispositivo da configurare" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "L'adattatore Bluetooth da utilizzare per la scansione" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index 99ad564ebd9..f7e6fe060af 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "no_adapters": "Nie znaleziono adapter\u00f3w Bluetooth" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Wybierz urz\u0105dzenie do skonfigurowania" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Adapter Bluetooth u\u017cywany do skanowania" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json index 972b858718c..802470d7c29 100644 --- a/homeassistant/components/bluetooth/translations/ru.json +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "no_adapters": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440\u044b Bluetooth \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index c08143d6b41..3c919e12d84 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -8,9 +8,15 @@ "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u043b\u0438\u0442\u044c \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432" } } - } + }, + "title": "\u0416\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0437\u0430\u043a\u0430\u043d\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f" + }, + "transmogrifier_deprecated": { + "description": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0442\u0440\u0430\u043d\u0441\u043c\u043e\u0433\u0440\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u043b \u0432 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435\u043c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0432 \u043d\u043e\u0432\u043e\u043c API.", + "title": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0442\u0440\u0430\u043d\u0441\u043c\u043e\u0433\u0440\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u043b" }, "unfixable_problem": { + "description": "\u042d\u0442\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u043e\u0442\u0441\u0442\u0443\u043f\u0438\u0442.", "title": "\u042d\u0442\u043e \u043d\u0435 \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" } }, diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 8ba4a573cec..ea0a3105226 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -9,7 +9,7 @@ "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_psk": "\u041a\u043b\u044e\u0447 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043e\u043d \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0432 \u0412\u0430\u0448\u0435\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", - "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." + "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 [ESPHome](https://esphomelib.com/)." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index 9900d3eb6d1..2e81b2357c8 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration des Google Kalenders in configuration.yaml wird in Home Assistant 2022.9 entfernt. \n\nDeine bestehenden OAuth-Anwendungsdaten und Zugriffseinstellungen wurden automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Google Calendar YAML-Konfiguration wird entfernt" + }, + "removed_track_new_yaml": { + "description": "Du hast die Entit\u00e4tsverfolgung f\u00fcr Google Kalender in configuration.yaml deaktiviert, was nicht mehr unterst\u00fctzt wird. Du musst die Integrationssystemoptionen in der Benutzeroberfl\u00e4che manuell \u00e4ndern, um neu entdeckte Entit\u00e4ten in Zukunft zu deaktivieren. Entferne die Einstellung track_new aus configuration.yaml und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Google Calendar Entity Tracking hat sich ge\u00e4ndert" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index 21e5580da3d..0bf592d60d4 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.9. \n\n \u03a4\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 OAuth \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "removed_track_new_yaml": { + "description": "\u0388\u03c7\u03b5\u03c4\u03b5 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf Google \u03c3\u03c4\u03bf configuration.yaml, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03c4\u03b9\u03c2 \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 track_new \u03b1\u03c0\u03cc \u03c4\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index bd1769fcbe0..4ce207ccd5b 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -39,8 +39,8 @@ "title": "The Google Calendar YAML configuration is being removed" }, "removed_track_new_yaml": { - "description": "Your Google Calendar configuration.yaml has disabled new entity tracking, which is no longer supported. You must manually set the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue.", - "title": "The Google Calendar entity tracking has changed" + "description": "You have disabled entity tracking for Google Calendar in configuration.yaml, which is no longer supported. You must manually change the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue.", + "title": "Google Calendar entity tracking has changed" } }, "options": { diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index 782fed55d5b..c29eb8d1a2c 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Google Calendar \u00e8 stata rimossa" + }, + "removed_track_new_yaml": { + "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "Il tracciamento dell'entit\u00e0 di Google Calendar \u00e8 cambiato" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json index ff7b8af3bfc..fb85af430df 100644 --- a/homeassistant/components/google/translations/pl.json +++ b/homeassistant/components/google/translations/pl.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Kalendarza Google w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2022.9. \n\nTwoje istniej\u0105ce po\u015bwiadczenia aplikacji OAuth i ustawienia dost\u0119pu zosta\u0142y automatycznie zaimportowane do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Kalendarza Google zostanie usuni\u0119ta" + }, + "removed_track_new_yaml": { + "description": "Wy\u0142\u0105czy\u0142e\u015b \u015bledzenie encji w Kalendarzu Google w pliku configuration.yaml, kt\u00f3ry nie jest ju\u017c obs\u0142ugiwany. Musisz r\u0119cznie zmieni\u0107 ustawienie w Opcjach Systemu integracji, aby wy\u0142\u0105czy\u0107 nowo wykryte encje w przysz\u0142o\u015bci. Usu\u0144 ustawienie track_new z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "\u015aledzenie encji Kalendarza Google uleg\u0142o zmianie" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index 83d4bb54afa..0b115c46423 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Google Agenda em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Google Agenda est\u00e1 sendo removida" + }, + "removed_track_new_yaml": { + "description": "Voc\u00ea desativou as entidades de rastreamento para o Google Agenda em configuration.yaml, que n\u00e3o \u00e9 mais compat\u00edvel. Voc\u00ea deve alterar manualmente as op\u00e7\u00f5es do sistema de integra\u00e7\u00e3o na interface do usu\u00e1rio para desativar as entidades rec\u00e9m-descobertas daqui para frente. Remova a configura\u00e7\u00e3o track_new de configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A entidade de rastreamento do Google Agenda foi alterado" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 5fc2cb03feb..57a6791cedf 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "removed_track_new_yaml": { + "description": "\u0412\u044b \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u043b\u0438 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f Google Calendar \u0432 \u0444\u0430\u0439\u043b\u0435 configuration.yaml, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0427\u0442\u043e\u0431\u044b \u043d\u043e\u0432\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043d\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u043b\u0438\u0441\u044c \u0432 Home Assistant \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 track_new \u0438\u0437 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 Google Calendar" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 45dc83c1b3c..43c208d69e8 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + }, + "removed_track_new_yaml": { + "description": "\u65bc configuration.yaml \u5167\u6240\u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u529f\u80fd\uff0c\u7531\u65bc\u4e0d\u518d\u652f\u6301\u3001\u5df2\u7d93\u906d\u5230\u95dc\u9589\u3002\u4e4b\u5f8c\u5fc5\u9808\u624b\u52d5\u900f\u904e\u4ecb\u9762\u5167\u7684\u6574\u5408\u529f\u80fd\u3001\u4ee5\u95dc\u9589\u4efb\u4f55\u65b0\u767c\u73fe\u7684\u5be6\u9ad4\u3002\u8acb\u7531 configuration.yaml \u4e2d\u79fb\u9664R track_new \u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u5df2\u7d93\u8b8a\u66f4" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/govee_ble/translations/pl.json b/homeassistant/components/govee_ble/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/ru.json b/homeassistant/components/govee_ble/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index d4ab6771ee5..00a4d9fcb7c 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -18,7 +18,7 @@ "unable_to_pair": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", "unknown_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u043e\u043e\u0431\u0449\u0438\u043b\u043e \u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435. \u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0432\u0441\u0435\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430\u0445 \u0438\u043b\u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u043c\u0438 \u043a\u043e\u0434\u0430\u043c\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", "pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, - "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441 {name} \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440. \u042d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u043b\u0438 \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0435.", + "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441 {name} ({category}) \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440. \u042d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u043b\u0438 \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0435.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u0432 HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index 1ca87d9ac80..d5c5bef24fa 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -102,7 +102,7 @@ "multicast_group": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `224.0.23.12`", "multicast_port": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `3671`", "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443.\n\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f: \u043e\u0442 20 \u0434\u043e 40", - "state_updater": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0447\u0442\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438\u0437 \u0448\u0438\u043d\u044b KNX. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d, Home Assistant \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438\u0437 \u0448\u0438\u043d\u044b KNX, \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 sync_state \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u043c\u0435\u0442\u044c \u043d\u0438\u043a\u0430\u043a\u043e\u0433\u043e \u044d\u0444\u0444\u0435\u043a\u0442\u0430." + "state_updater": "\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0438\u0437 \u0448\u0438\u043d\u044b KNX. \u0415\u0441\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e, Home Assistant \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0441 \u0448\u0438\u043d\u044b KNX. \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043c\u0435\u043d\u0435\u043d \u043e\u043f\u0446\u0438\u044f\u043c\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 `sync_state`." } }, "tunnel": { diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index 1f1f92977ed..c0cd51a72d4 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -29,7 +29,7 @@ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", - "title": "Life360" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Life360" } } }, diff --git a/homeassistant/components/lyric/translations/el.json b/homeassistant/components/lyric/translations/el.json index e9321950874..a16d2844dcb 100644 --- a/homeassistant/components/lyric/translations/el.json +++ b/homeassistant/components/lyric/translations/el.json @@ -17,5 +17,11 @@ "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } + }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Honeywell Lyric \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03b7\u03c2 Honeywell Lyric \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json index 6fb3fb44275..e0e97ef3246 100644 --- a/homeassistant/components/lyric/translations/it.json +++ b/homeassistant/components/lyric/translations/it.json @@ -17,5 +17,11 @@ "title": "Autentica nuovamente l'integrazione" } } + }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Honeywell Lyric tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Honeywell Lyric \u00e8 stata rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pl.json b/homeassistant/components/lyric/translations/pl.json index 09ae3ba273a..bddf67f62e8 100644 --- a/homeassistant/components/lyric/translations/pl.json +++ b/homeassistant/components/lyric/translations/pl.json @@ -17,5 +17,11 @@ "title": "Ponownie uwierzytelnij integracj\u0119" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Honeywell Lyric za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Honeywell Lyric zosta\u0142a usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/de.json b/homeassistant/components/miflora/translations/de.json new file mode 100644 index 00000000000..8120ddcd25a --- /dev/null +++ b/homeassistant/components/miflora/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Die Mi Flora-Integration funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE-Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Mi Flora-Ger\u00e4t mit der neuen Integration manuell hinzuf\u00fcgen.\n\nDeine bestehende Mi Flora YAML-Konfiguration wird vom Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Mi Flora-Integration wurde ersetzt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/el.json b/homeassistant/components/miflora/translations/el.json new file mode 100644 index 00000000000..bf4076979d1 --- /dev/null +++ b/homeassistant/components/miflora/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Mi Flora \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant 2022.7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Xiaomi BLE \u03c3\u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 2022.8. \n\n \u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 Mi Flora \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Mi Flora YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Mi Flora \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/it.json b/homeassistant/components/miflora/translations/it.json new file mode 100644 index 00000000000..cdd2d89ca72 --- /dev/null +++ b/homeassistant/components/miflora/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "L'integrazione Mi Flora ha smesso di funzionare in Home Assistant 2022.7 e sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Mi Flora utilizzando la nuova integrazione. \n\nLa configurazione YAML di Mi Flora esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione Mi Flora \u00e8 stata sostituita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/pl.json b/homeassistant/components/miflora/translations/pl.json new file mode 100644 index 00000000000..d8b80bdd26d --- /dev/null +++ b/homeassistant/components/miflora/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integracja Mi Flora przesta\u0142a dzia\u0142a\u0107 w Home Assistant 2022.7 i zosta\u0142a zast\u0105piona integracj\u0105 Xiaomi BLE w wydaniu 2022.8. \n\nNie ma mo\u017cliwo\u015bci migracji, dlatego musisz r\u0119cznie doda\u0107 urz\u0105dzenie Mi Flora za pomoc\u0105 nowej integracji. \n\nTwoja istniej\u0105ca konfiguracja YAML dla Mi Flora nie jest ju\u017c u\u017cywana przez Home Assistanta. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Mi Flora zosta\u0142a zast\u0105piona" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/pt-BR.json b/homeassistant/components/miflora/translations/pt-BR.json new file mode 100644 index 00000000000..3b8b4658040 --- /dev/null +++ b/homeassistant/components/miflora/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A integra\u00e7\u00e3o do Mi Flora parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Mi Flora usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o existente do Mi Flora YAML n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do Mi Flora foi substitu\u00edda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/zh-Hant.json b/homeassistant/components/miflora/translations/zh-Hant.json new file mode 100644 index 00000000000..e6af26efcb9 --- /dev/null +++ b/homeassistant/components/miflora/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u82b1\u82b1\u8349\u8349 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/de.json b/homeassistant/components/mitemp_bt/translations/de.json new file mode 100644 index 00000000000..7d4887ab638 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t manuell mit der neuen Integration hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors wurde ersetzt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/el.json b/homeassistant/components/mitemp_bt/translations/el.json new file mode 100644 index 00000000000..f8ac6849c10 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 Xiaomi Mijia BLE \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant 2022.7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Xiaomi BLE \u03c3\u03c4\u03b7\u03bd \u03ba\u03c5\u03ba\u03bb\u03bf\u03c6\u03bf\u03c1\u03af\u03b1 \u03c4\u03bf\u03c5 2022.8. \n\n \u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Mijia BLE \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 Xiaomi Mijia BLE \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 Xiaomi Mijia BLE \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pl.json b/homeassistant/components/mitemp_bt/translations/pl.json new file mode 100644 index 00000000000..08770ff011b --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integracja sensora temperatury i wilgotno\u015bci Xiaomi Mijia BLE przesta\u0142a dzia\u0142a\u0107 w Home Assistant 2022.7 i zosta\u0142a zast\u0105piona integracj\u0105 Xiaomi BLE w wersji 2022.8. \n\nNie ma mo\u017cliwo\u015bci migracji, dlatego musisz r\u0119cznie doda\u0107 swoje urz\u0105dzenie Xiaomi Mijia BLE przy u\u017cyciu nowej integracji. \n\nTwoja istniej\u0105ca konfiguracja YAML dla sensora temperatury i wilgotno\u015bci Xiaomi Mijia BLE nie jest ju\u017c u\u017cywana przez Home Assistanta. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja sensora temperatury i wilgotno\u015bci Xiaomi Mijia BLE zosta\u0142a zast\u0105piona" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pt-BR.json b/homeassistant/components/mitemp_bt/translations/pt-BR.json new file mode 100644 index 00000000000..991a749a729 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE foi substitu\u00edda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/zh-Hant.json b/homeassistant/components/mitemp_bt/translations/zh-Hant.json new file mode 100644 index 00000000000..05799bbd712 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/pl.json b/homeassistant/components/moat/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/moat/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/ru.json b/homeassistant/components/moat/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/moat/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 0cb2c91d819..e071637713f 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -23,7 +23,7 @@ "subscriber_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0433\u043e \u043f\u043e\u0434\u043f\u0438\u0441\u0447\u0438\u043a\u0430. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0445.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", - "wrong_project_id": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 Cloud Project ID (\u043d\u0430\u0439\u0434\u0435\u043d Device Access Project ID)" + "wrong_project_id": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 Cloud Project ID (\u0442\u0430\u043a\u043e\u0439 \u0436\u0435 \u043a\u0430\u043a \u0438 Device Access Project ID)" }, "step": { "auth": { @@ -44,7 +44,19 @@ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 *example-project-12345*. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 [Google Cloud Console({cloud_console_url}) \u0438\u043b\u0438 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e]({more_info_url}).", "title": "Nest: \u0432\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID" }, + "create_cloud_project": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Nest \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u044b, \u043a\u0430\u043c\u0435\u0440\u044b \u0438 \u0434\u0432\u0435\u0440\u043d\u044b\u0435 \u0437\u0432\u043e\u043d\u043a\u0438 Nest \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Smart Device Management. SDM API **\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0435\u0434\u0438\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043f\u043b\u0430\u0442\u044b \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 US $5**. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({more_info_url}).\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [Google Cloud Console]({cloud_console_url}).\n2. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u0412\u0430\u0448 \u043f\u0435\u0440\u0432\u044b\u0439 \u043f\u0440\u043e\u0435\u043a\u0442, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Create Project**, \u0437\u0430\u0442\u0435\u043c **New Project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Create**.\n4. \u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u0435 Cloud Project ID, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, *example-project-12345*, \u043e\u043d \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0412\u0430\u043c \u043f\u043e\u0437\u0436\u0435.\n5. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 API \u0434\u043b\u044f [Smart Device Management API]({sdm_api_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Enable**.\n6. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 API \u0434\u043b\u044f [Cloud Pub/Sub API]({pubsub_api_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Enable**.\n\n\u041f\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044e \u0432\u044b\u0448\u0435\u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0448\u0430\u0433\u043e\u0432 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.", + "title": "Nest: \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430" + }, + "device_project": { + "data": { + "project_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e **\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043b\u0430\u0442\u0430 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 5 \u0434\u043e\u043b\u043b\u0430\u0440\u043e\u0432 \u0421\u0428\u0410**.\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u041a\u043e\u043d\u0441\u043e\u043b\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c]({device_access_console_url}) \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043e\u043f\u043b\u0430\u0442\u044b.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 **Create project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Next**.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth.\n5. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043d\u0430\u0436\u0430\u0432 **Enable** \u0438 **Create project**. \n\n\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c \u043d\u0438\u0436\u0435 ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).", + "title": "Nest: \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, "device_project_upgrade": { + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0443\u043a\u0430\u0437\u0430\u0432 \u043d\u043e\u0432\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u041a\u043e\u043d\u0441\u043e\u043b\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c]({device_access_console_url}).\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a \u043a\u043e\u0440\u0437\u0438\u043d\u044b \u0440\u044f\u0434\u043e\u043c \u0441 *OAuth Client ID*.\n3. \u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043c\u0435\u043d\u044e, \u043d\u0430\u0436\u0430\u0432 \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0443 `...`, \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 *Add Client ID*.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Add**. \n\n\u0412\u0430\u0448 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth: `{client_id}`", "title": "Nest: \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c" }, "init": { diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 92d78e7f4ad..a6a31b7b63a 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -20,8 +20,8 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Smile" }, - "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:", - "title": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Plugwise" + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/radiotherm/translations/el.json b/homeassistant/components/radiotherm/translations/el.json index 9b276da3670..8e60a12d1e8 100644 --- a/homeassistant/components/radiotherm/translations/el.json +++ b/homeassistant/components/radiotherm/translations/el.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03bb\u03b9\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae\u03c2 \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 \u03c1\u03b1\u03b4\u03b9\u03bf\u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c1\u03b1\u03b4\u03b9\u03bf\u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index 653dd56321b..9e35beb648a 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML \u00e8 stata rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/pl.json b/homeassistant/components/radiotherm/translations/pl.json index e69568131d6..4265577b46e 100644 --- a/homeassistant/components/radiotherm/translations/pl.json +++ b/homeassistant/components/radiotherm/translations/pl.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja platformy klimatycznej Radio Thermostat za pomoc\u0105 YAML zostanie usuni\u0119ta w Home Assistant 2022.9. \n\nTwoja istniej\u0105ca konfiguracja zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Radio Thermostat zostanie usuni\u0119ta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/ru.json b/homeassistant/components/radiotherm/translations/ru.json index daac6c7c5ae..6d4615f9820 100644 --- a/homeassistant/components/radiotherm/translations/ru.json +++ b/homeassistant/components/radiotherm/translations/ru.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/senz/translations/el.json b/homeassistant/components/senz/translations/el.json index cd34896da39..b6ae9a85e99 100644 --- a/homeassistant/components/senz/translations/el.json +++ b/homeassistant/components/senz/translations/el.json @@ -16,5 +16,11 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" } } + }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 nVent RAYCHEM SENZ \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd YAML \u03c4\u03bf\u03c5 nVent RAYCHEM SENZ \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/it.json b/homeassistant/components/senz/translations/it.json index c92ebb2a57c..be6608af499 100644 --- a/homeassistant/components/senz/translations/it.json +++ b/homeassistant/components/senz/translations/it.json @@ -16,5 +16,11 @@ "title": "Scegli il metodo di autenticazione" } } + }, + "issues": { + "removed_yaml": { + "description": "La configurazione di nVent RAYCHEM SENZ tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di nVent RAYCHEM SENZ \u00e8 stata rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pl.json b/homeassistant/components/senz/translations/pl.json index d58148cb8fa..79fd5e3a29c 100644 --- a/homeassistant/components/senz/translations/pl.json +++ b/homeassistant/components/senz/translations/pl.json @@ -16,5 +16,11 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja nVent RAYCHEM SENZ za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla nVent RAYCHEM SENZ zosta\u0142a usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index d852664ce38..ecc263e6821 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 SimpliSafe \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7.", "email_2fa_timed_out": "\u03a4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03ad\u03bb\u03b7\u03be\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 email.", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "wrong_account": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc SimpliSafe." }, "error": { + "identifier_exists": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, @@ -28,6 +30,7 @@ }, "user": { "data": { + "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "Email" }, diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 22c4739b7dd..04f6e8dd44b 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu", "email_2fa_timed_out": "Przekroczono limit czasu oczekiwania na e-mailowe uwierzytelnianie dwusk\u0142adnikowe.", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "wrong_account": "Podane dane uwierzytelniaj\u0105ce u\u017cytkownika nie pasuj\u0105 do tego konta SimpliSafe." }, "error": { + "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Kod autoryzacji", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o." + "description": "SimpliSafe uwierzytelnia u\u017cytkownik\u00f3w za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) przed rozpocz\u0119ciem. \n\nGdy b\u0119dziesz ju\u017c gotowy, kliknij [tutaj]({url}), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Po zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i wprowad\u017a kod autoryzacji z adresu URL aplikacji internetowej SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 198d5225983..1b88417dd27 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "email_2fa_timed_out": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "wrong_account": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 SimpliSafe." }, "error": { + "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." + "description": "SimpliSafe \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u0441\u0432\u043e\u0435 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 [\u0441\u044e\u0434\u0430]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SimpliSafe." } } }, diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index 799f3717b81..9d48e7b58e2 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Spotify \u00fcber YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird von Home Assistant nicht verwendet.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Spotify-YAML-Konfiguration wurde entfernt" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify-API-Endpunkt erreichbar" diff --git a/homeassistant/components/spotify/translations/el.json b/homeassistant/components/spotify/translations/el.json index 1b9aadb7caf..1c89861b325 100644 --- a/homeassistant/components/spotify/translations/el.json +++ b/homeassistant/components/spotify/translations/el.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Spotify \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Spotify YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + }, "system_health": { "info": { "api_endpoint_reachable": "\u03a4\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c4\u03bf\u03c5 Spotify API \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf" diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index 445b7a233e4..2ca8d323607 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Spotify tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Spotify \u00e8 stata rimossa" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Endpoint API Spotify raggiungibile" diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json index 52028d4d368..bbdda0ae586 100644 --- a/homeassistant/components/spotify/translations/pl.json +++ b/homeassistant/components/spotify/translations/pl.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Spotify za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Spotify zosta\u0142a usuni\u0119ta" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Punkt ko\u0144cowy Spotify API osi\u0105galny" diff --git a/homeassistant/components/spotify/translations/pt-BR.json b/homeassistant/components/spotify/translations/pt-BR.json index c49fbfabfa0..97948f71641 100644 --- a/homeassistant/components/spotify/translations/pt-BR.json +++ b/homeassistant/components/spotify/translations/pt-BR.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Spotify usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Spotify YAML foi removida" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Endpoint da API do Spotify acess\u00edvel" diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index fc9362697e2..35918b2634f 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Spotify \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } + }, "system_health": { "info": { "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a API Spotify" diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index ce35507e661..ce89e224a8c 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Spotify YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API \u53ef\u9054\u7aef\u9ede" diff --git a/homeassistant/components/steam_online/translations/de.json b/homeassistant/components/steam_online/translations/de.json index 44fb7f8b08b..aef08bfe269 100644 --- a/homeassistant/components/steam_online/translations/de.json +++ b/homeassistant/components/steam_online/translations/de.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Steam \u00fcber YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird von Home Assistant nicht verwendet.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Steam-YAML-Konfiguration wurde entfernt" + } + }, "options": { "error": { "unauthorized": "Freundesliste eingeschr\u00e4nkt: Bitte lies in der Dokumentation nach, wie du alle anderen Freunde sehen kannst" diff --git a/homeassistant/components/steam_online/translations/el.json b/homeassistant/components/steam_online/translations/el.json index 02405dc0215..33a864e0945 100644 --- a/homeassistant/components/steam_online/translations/el.json +++ b/homeassistant/components/steam_online/translations/el.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Steam \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd Steam YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + }, "options": { "error": { "unauthorized": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c6\u03af\u03bb\u03c9\u03bd: \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03ac\u03bb\u03bb\u03bf\u03c5\u03c2 \u03c6\u03af\u03bb\u03bf\u03c5\u03c2" diff --git a/homeassistant/components/steam_online/translations/it.json b/homeassistant/components/steam_online/translations/it.json index 3f2e9481301..fb962124f2a 100644 --- a/homeassistant/components/steam_online/translations/it.json +++ b/homeassistant/components/steam_online/translations/it.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Steam tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Steam \u00e8 stata rimossa" + } + }, "options": { "error": { "unauthorized": "Elenco amici limitato: consultare la documentazione su come vedere tutti gli altri amici." diff --git a/homeassistant/components/steam_online/translations/pl.json b/homeassistant/components/steam_online/translations/pl.json index 3ceff9e442f..08c05b7de41 100644 --- a/homeassistant/components/steam_online/translations/pl.json +++ b/homeassistant/components/steam_online/translations/pl.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Steam za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Steam zosta\u0142a usuni\u0119ta" + } + }, "options": { "error": { "unauthorized": "Lista znajomych ograniczona: zapoznaj si\u0119 z dokumentacj\u0105, aby dowiedzie\u0107 si\u0119, jak zobaczy\u0107 wszystkich innych znajomych" diff --git a/homeassistant/components/steam_online/translations/pt-BR.json b/homeassistant/components/steam_online/translations/pt-BR.json index 5aa9d0d2520..9afd81f4a5f 100644 --- a/homeassistant/components/steam_online/translations/pt-BR.json +++ b/homeassistant/components/steam_online/translations/pt-BR.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o da Steam usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o da Steam YAML foi removida" + } + }, "options": { "error": { "unauthorized": "Lista restrita de amigos: consulte a documenta\u00e7\u00e3o sobre como ver todos os outros amigos" diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index b3d84b7f9e6..b69b1a72696 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Steam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } + }, "options": { "error": { "unauthorized": "\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0440\u0443\u0437\u0435\u0439 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0432\u0441\u0435\u0445 \u0434\u0440\u0443\u0437\u044c\u044f\u0445." diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json index 7de1d6f1a3c..e3c725532e9 100644 --- a/homeassistant/components/steam_online/translations/zh-Hant.json +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Steam YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + } + }, "options": { "error": { "unauthorized": "\u597d\u53cb\u5217\u8868\u53d7\u9650\uff1a\u8acb\u53c3\u8003\u6587\u4ef6\u8cc7\u6599\u4ee5\u4e86\u89e3\u5982\u4f55\u986f\u793a\u6240\u6709\u597d\u53cb\u3002" diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index b191c90f53c..585dd3ccbd4 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "mac": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 156ddbb9924..dc43a7f610d 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -13,10 +13,11 @@ "one": "Pusty", "other": "" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Adres urz\u0105dzenia", "mac": "Adres MAC urz\u0105dzenia", "name": "Nazwa", "password": "Has\u0142o" diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index 9ca076ff499..7b2f4f73cc2 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0439 \u0442\u0438\u043f Switchbot.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "mac": "MAC-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" diff --git a/homeassistant/components/xiaomi_ble/translations/el.json b/homeassistant/components/xiaomi_ble/translations/el.json index 0a802a0bc89..30dc61cb5dc 100644 --- a/homeassistant/components/xiaomi_ble/translations/el.json +++ b/homeassistant/components/xiaomi_ble/translations/el.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "decryption_failed": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5, \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b1\u03bd \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "expected_24_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", + "expected_32_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03bf \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03ad\u03c3\u03bc\u03b5\u03c5\u03c3\u03b7\u03c2 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd." + }, "user": { "data": { "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/xiaomi_ble/translations/pl.json b/homeassistant/components/xiaomi_ble/translations/pl.json index 51168716783..2ca956019ef 100644 --- a/homeassistant/components/xiaomi_ble/translations/pl.json +++ b/homeassistant/components/xiaomi_ble/translations/pl.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", + "expected_24_characters": "Oczekiwano 24-znakowego szesnastkowego klucza bindkey.", + "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey.", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 32-znakowego szesnastkowego klucza bindkey." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 24-znakowego szesnastkowego klucza bindkey." + }, "user": { "data": { "address": "Urz\u0105dzenie" diff --git a/homeassistant/components/xiaomi_ble/translations/ru.json b/homeassistant/components/xiaomi_ble/translations/ru.json index c912fc120e4..a90da71d84e 100644 --- a/homeassistant/components/xiaomi_ble/translations/ru.json +++ b/homeassistant/components/xiaomi_ble/translations/ru.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "decryption_failed": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b, \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "expected_24_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", + "expected_32_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438" + }, + "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438" + }, + "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." + }, "user": { "data": { "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 30c85de48da..4666ba7e494 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -46,6 +46,7 @@ "title": "Opzioni del pannello di controllo degli allarmi" }, "zha_options": { + "always_prefer_xy_color_mode": "Preferisci sempre la modalit\u00e0 colore XY", "consider_unavailable_battery": "Considera i dispositivi alimentati a batteria non disponibili dopo (secondi)", "consider_unavailable_mains": "Considera i dispositivi alimentati dalla rete non disponibili dopo (secondi)", "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", From 89493f2d7f64110288db4253a2909f5d73c30c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Tue, 26 Jul 2022 20:36:41 -0700 Subject: [PATCH 724/820] Add occupancy sensor to the BAF integration (#75793) Co-authored-by: J. Nick Koston --- homeassistant/components/baf/__init__.py | 1 + homeassistant/components/baf/binary_sensor.py | 80 +++++++++++++++++++ homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/baf/binary_sensor.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 9bb056ece4f..7e80341deab 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -15,6 +15,7 @@ from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.FAN, Platform.LIGHT, diff --git a/homeassistant/components/baf/binary_sensor.py b/homeassistant/components/baf/binary_sensor.py new file mode 100644 index 00000000000..8a00a7e8bd5 --- /dev/null +++ b/homeassistant/components/baf/binary_sensor.py @@ -0,0 +1,80 @@ +"""Support for Big Ass Fans binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Optional, cast + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BAFEntity +from .models import BAFData + + +@dataclass +class BAFBinarySensorDescriptionMixin: + """Required values for BAF binary sensors.""" + + value_fn: Callable[[Device], bool | None] + + +@dataclass +class BAFBinarySensorDescription( + BinarySensorEntityDescription, + BAFBinarySensorDescriptionMixin, +): + """Class describing BAF binary sensor entities.""" + + +OCCUPANCY_SENSORS = ( + BAFBinarySensorDescription( + key="occupancy", + name="Occupancy", + device_class=BinarySensorDeviceClass.OCCUPANCY, + value_fn=lambda device: cast(Optional[bool], device.fan_occupancy_detected), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF binary sensors.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + sensors_descriptions: list[BAFBinarySensorDescription] = [] + if device.has_occupancy: + sensors_descriptions.extend(OCCUPANCY_SENSORS) + async_add_entities( + BAFBinarySensor(device, description) for description in sensors_descriptions + ) + + +class BAFBinarySensor(BAFEntity, BinarySensorEntity): + """BAF binary sensor.""" + + entity_description: BAFBinarySensorDescription + + def __init__(self, device: Device, description: BAFBinarySensorDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, f"{device.name} {description.name}") + self._attr_unique_id = f"{self._device.mac_address}-{description.key}" + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + description = self.entity_description + self._attr_is_on = description.value_fn(self._device) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 821ad1a21cb..15e0272b2b0 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.6.0"], + "requirements": ["aiobafi6==0.7.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7a8f314df9f..f1e14dc0aeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -128,7 +128,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.6.0 +aiobafi6==0.7.0 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc952f553db..d52446c4a26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.6.0 +aiobafi6==0.7.0 # homeassistant.components.aws aiobotocore==2.1.0 From 5e6217f20ce90f952a647ea9bd446f00026f6168 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 27 Jul 2022 14:13:38 +0800 Subject: [PATCH 725/820] Use executor to finish stream recording (#75776) --- homeassistant/components/stream/recorder.py | 44 +++++++++++++-------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 42b98946c15..3bda8acfa7b 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -4,6 +4,7 @@ from __future__ import annotations from io import BytesIO import logging import os +from typing import TYPE_CHECKING import av @@ -16,6 +17,9 @@ from .const import ( ) from .core import PROVIDERS, IdleTimer, Segment, StreamOutput, StreamSettings +if TYPE_CHECKING: + import deque + _LOGGER = logging.getLogger(__name__) @@ -139,6 +143,25 @@ class RecorderOutput(StreamOutput): source.close() + def finish_writing( + segments: deque[Segment], output: av.OutputContainer, video_path: str + ) -> None: + """Finish writing output.""" + # Should only have 0 or 1 segments, but loop through just in case + while segments: + write_segment(segments.popleft()) + if output is None: + _LOGGER.error("Recording failed to capture anything") + return + output.close() + try: + os.rename(video_path + ".tmp", video_path) + except FileNotFoundError: + _LOGGER.error( + "Error writing to '%s'. There are likely multiple recordings writing to the same file", + video_path, + ) + # Write lookback segments while len(self._segments) > 1: # The last segment is in progress await self._hass.async_add_executor_job( @@ -153,20 +176,7 @@ class RecorderOutput(StreamOutput): await self._hass.async_add_executor_job( write_segment, self._segments.popleft() ) - # Write remaining segments - # Should only have 0 or 1 segments, but loop through just in case - while self._segments: - await self._hass.async_add_executor_job( - write_segment, self._segments.popleft() - ) - if output is None: - _LOGGER.error("Recording failed to capture anything") - else: - output.close() - try: - os.rename(self.video_path + ".tmp", self.video_path) - except FileNotFoundError: - _LOGGER.error( - "Error writing to '%s'. There are likely multiple recordings writing to the same file", - self.video_path, - ) + # Write remaining segments and close output + await self._hass.async_add_executor_job( + finish_writing, self._segments, output, self.video_path + ) From 56871507862def17e3a37a7f8f097a8192303361 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 27 Jul 2022 10:01:09 +0200 Subject: [PATCH 726/820] Add state to CheckControlMessages in bmw_connected_drive (#75802) Co-authored-by: rikroe --- .../components/bmw_connected_drive/binary_sensor.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index a19ccc8f715..94d8902fe23 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -39,14 +39,8 @@ def _condition_based_services( def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]: extra_attributes: dict[str, Any] = {} - if vehicle.check_control_messages.has_check_control_messages: - cbs_list = [ - message.description_short - for message in vehicle.check_control_messages.messages - ] - extra_attributes["check_control_messages"] = cbs_list - else: - extra_attributes["check_control_messages"] = "OK" + for message in vehicle.check_control_messages.messages: + extra_attributes.update({message.description_short: message.state.value}) return extra_attributes From bbdce93291d50dbe99a5d3af4c1bf39c02623628 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 27 Jul 2022 03:01:44 -0500 Subject: [PATCH 727/820] Fix error on shutdown when no Sonos devices available (#75798) --- homeassistant/components/sonos/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 837b6c0f749..d94b49e52f2 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -201,7 +201,11 @@ class SonosDiscoveryManager: speaker.activity_stats.log_report() speaker.event_stats.log_report() if zgs := next( - speaker.soco.zone_group_state for speaker in self.data.discovered.values() + ( + speaker.soco.zone_group_state + for speaker in self.data.discovered.values() + ), + None, ): _LOGGER.debug( "ZoneGroupState stats: (%s/%s) processed", From 51c3836ec2dbf14afa5f158bcc41052b0b41f26a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 27 Jul 2022 10:13:16 +0200 Subject: [PATCH 728/820] Create Repairs based on Alerts (#75397) Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/default_config/manifest.json | 1 + .../homeassistant_alerts/__init__.py | 185 ++++++++ .../homeassistant_alerts/manifest.json | 8 + .../homeassistant_alerts/strings.json | 8 + mypy.ini | 11 + script/hassfest/manifest.py | 1 + .../homeassistant_alerts/__init__.py | 1 + .../fixtures/alerts_1.json | 174 +++++++ .../fixtures/alerts_2.json | 159 +++++++ .../fixtures/alerts_no_integrations.json | 27 ++ .../fixtures/alerts_no_package.json | 33 ++ .../fixtures/alerts_no_url.json | 34 ++ .../homeassistant_alerts/test_init.py | 433 ++++++++++++++++++ 15 files changed, 1078 insertions(+) create mode 100644 homeassistant/components/homeassistant_alerts/__init__.py create mode 100644 homeassistant/components/homeassistant_alerts/manifest.json create mode 100644 homeassistant/components/homeassistant_alerts/strings.json create mode 100644 tests/components/homeassistant_alerts/__init__.py create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_1.json create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_2.json create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_package.json create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_url.json create mode 100644 tests/components/homeassistant_alerts/test_init.py diff --git a/.strict-typing b/.strict-typing index 45d11f089dd..c5b4e376414 100644 --- a/.strict-typing +++ b/.strict-typing @@ -115,6 +115,7 @@ homeassistant.components.group.* homeassistant.components.guardian.* homeassistant.components.history.* homeassistant.components.homeassistant.triggers.event +homeassistant.components.homeassistant_alerts.* homeassistant.components.homekit homeassistant.components.homekit.accessories homeassistant.components.homekit.aidmanager diff --git a/CODEOWNERS b/CODEOWNERS index 3ad4c23816c..bd39fd68590 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -455,6 +455,8 @@ build.json @home-assistant/supervisor /tests/components/home_plus_control/ @chemaaa /homeassistant/components/homeassistant/ @home-assistant/core /tests/components/homeassistant/ @home-assistant/core +/homeassistant/components/homeassistant_alerts/ @home-assistant/core +/tests/components/homeassistant_alerts/ @home-assistant/core /homeassistant/components/homeassistant_yellow/ @home-assistant/core /tests/components/homeassistant_yellow/ @home-assistant/core /homeassistant/components/homekit/ @bdraco diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 3cb9e60a278..f790292c27a 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -11,6 +11,7 @@ "dhcp", "energy", "frontend", + "homeassistant_alerts", "history", "input_boolean", "input_button", diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py new file mode 100644 index 00000000000..1aedd6c5419 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -0,0 +1,185 @@ +"""The Home Assistant alerts integration.""" +from __future__ import annotations + +import asyncio +import dataclasses +from datetime import timedelta +import logging + +import aiohttp +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.components.repairs import async_create_issue, async_delete_issue +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.const import __version__ +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.yaml import parse_yaml + +DOMAIN = "homeassistant_alerts" +UPDATE_INTERVAL = timedelta(hours=3) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up alerts.""" + last_alerts: dict[str, str | None] = {} + + async def async_update_alerts() -> None: + nonlocal last_alerts + + active_alerts: dict[str, str | None] = {} + + for issue_id, alert in coordinator.data.items(): + # Skip creation if already created and not updated since then + if issue_id in last_alerts and alert.date_updated == last_alerts[issue_id]: + active_alerts[issue_id] = alert.date_updated + continue + + # Fetch alert to get title + description + try: + response = await async_get_clientsession(hass).get( + f"https://alerts.home-assistant.io/alerts/{alert.filename}", + timeout=aiohttp.ClientTimeout(total=10), + ) + except asyncio.TimeoutError: + _LOGGER.warning("Error fetching %s: timeout", alert.filename) + continue + + alert_content = await response.text() + alert_parts = alert_content.split("---") + + if len(alert_parts) != 3: + _LOGGER.warning( + "Error parsing %s: unexpected metadata format", alert.filename + ) + continue + + try: + alert_info = parse_yaml(alert_parts[1]) + except ValueError as err: + _LOGGER.warning("Error parsing %s metadata: %s", alert.filename, err) + continue + + if not isinstance(alert_info, dict) or "title" not in alert_info: + _LOGGER.warning("Error in %s metadata: title not found", alert.filename) + continue + + alert_title = alert_info["title"] + alert_content = alert_parts[2].strip() + + async_create_issue( + hass, + DOMAIN, + issue_id, + is_fixable=False, + learn_more_url=alert.alert_url, + severity=IssueSeverity.WARNING, + translation_key="alert", + translation_placeholders={ + "title": alert_title, + "description": alert_content, + }, + ) + active_alerts[issue_id] = alert.date_updated + + inactive_alerts = last_alerts.keys() - active_alerts.keys() + for issue_id in inactive_alerts: + async_delete_issue(hass, DOMAIN, issue_id) + + last_alerts = active_alerts + + @callback + def async_schedule_update_alerts() -> None: + if not coordinator.last_update_success: + return + + hass.async_create_task(async_update_alerts()) + + coordinator = AlertUpdateCoordinator(hass) + coordinator.async_add_listener(async_schedule_update_alerts) + await coordinator.async_refresh() + + return True + + +@dataclasses.dataclass(frozen=True) +class IntegrationAlert: + """Issue Registry Entry.""" + + integration: str + filename: str + date_updated: str | None + alert_url: str | None + + @property + def issue_id(self) -> str: + """Return the issue id.""" + return f"{self.filename}_{self.integration}" + + +class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]): + """Data fetcher for HA Alerts.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the data updater.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=UPDATE_INTERVAL, + ) + self.ha_version = AwesomeVersion( + __version__, + ensure_strategy=AwesomeVersionStrategy.CALVER, + find_first_match=False, + ) + + async def _async_update_data(self) -> dict[str, IntegrationAlert]: + response = await async_get_clientsession(self.hass).get( + "https://alerts.home-assistant.io/alerts.json", + timeout=aiohttp.ClientTimeout(total=10), + ) + alerts = await response.json() + + result = {} + + for alert in alerts: + if "alert_url" not in alert or "integrations" not in alert: + continue + + if "homeassistant" in alert: + if "affected_from_version" in alert["homeassistant"]: + affected_from_version = AwesomeVersion( + alert["homeassistant"]["affected_from_version"], + find_first_match=False, + ) + if self.ha_version < affected_from_version: + continue + if "resolved_in_version" in alert["homeassistant"]: + resolved_in_version = AwesomeVersion( + alert["homeassistant"]["resolved_in_version"], + find_first_match=False, + ) + if self.ha_version >= resolved_in_version: + continue + + for integration in alert["integrations"]: + if "package" not in integration: + continue + + if integration["package"] not in self.hass.config.components: + continue + + integration_alert = IntegrationAlert( + integration=integration["package"], + filename=alert["filename"], + date_updated=alert.get("date_updated"), + alert_url=alert["alert_url"], + ) + + result[integration_alert.issue_id] = integration_alert + + return result diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json new file mode 100644 index 00000000000..0d276c6f3ae --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "homeassistant_alerts", + "name": "Home Assistant alerts", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts", + "codeowners": ["@home-assistant/core"], + "dependencies": ["repairs"] +} diff --git a/homeassistant/components/homeassistant_alerts/strings.json b/homeassistant/components/homeassistant_alerts/strings.json new file mode 100644 index 00000000000..7a9634d6268 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "title": "{title}", + "description": "{description}" + } + } +} diff --git a/mypy.ini b/mypy.ini index af6abe6658f..37765023f74 100644 --- a/mypy.ini +++ b/mypy.ini @@ -988,6 +988,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homeassistant_alerts.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homekit] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index c6cc019ff31..5ce67b59198 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -56,6 +56,7 @@ NO_IOT_CLASS = [ "hardware", "history", "homeassistant", + "homeassistant_alerts", "homeassistant_yellow", "image", "input_boolean", diff --git a/tests/components/homeassistant_alerts/__init__.py b/tests/components/homeassistant_alerts/__init__.py new file mode 100644 index 00000000000..e8e83fad6bd --- /dev/null +++ b/tests/components/homeassistant_alerts/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant alerts integration.""" diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_1.json b/tests/components/homeassistant_alerts/fixtures/alerts_1.json new file mode 100644 index 00000000000..381a31d7a5d --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_1.json @@ -0,0 +1,174 @@ +[ + { + "title": "Aladdin Connect is turning off their previous connection method", + "created": "2022-07-14T06:00:00.000Z", + "integrations": [ + { + "package": "aladdin_connect" + } + ], + "homeassistant": { + "package": "homeassistant", + "resolved_in_version": "2022.7" + }, + "filename": "aladdin_connect.markdown", + "alert_url": "https://alerts.home-assistant.io/#aladdin_connect.markdown" + }, + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + { + "package": "hikvisioncam" + } + ], + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Hive shutting down North American Servers", + "created": "2021-11-13T13:58:00.000Z", + "integrations": [ + { + "package": "hive" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "2021.11.0" + }, + "filename": "hive_us.markdown", + "alert_url": "https://alerts.home-assistant.io/#hive_us.markdown" + }, + { + "title": "HomematicIP (EQ-3) blocks public IP addresses, if access to the Cloud is too frequent.", + "created": "2020-12-20T12:00:00.000Z", + "integrations": [ + { + "package": "homematicip_cloud" + } + ], + "packages": [ + { + "package": "homematicip" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.7" + }, + "filename": "homematicip_cloud.markdown", + "alert_url": "https://alerts.home-assistant.io/#homematicip_cloud.markdown" + }, + { + "title": "Logitech no longer accepting API applications", + "created": "2022-05-16T12:00:00.000Z", + "integrations": [ + { + "package": "logi_circle" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/71945", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.92.0" + }, + "filename": "logi_circle.markdown", + "alert_url": "https://alerts.home-assistant.io/#logi_circle.markdown" + }, + { + "title": "New Neato Botvacs Do Not Support Existing API", + "created": "2021-12-20T13:27:00.000Z", + "integrations": [ + { + "package": "neato" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "neato.markdown", + "alert_url": "https://alerts.home-assistant.io/#neato.markdown" + }, + { + "title": "Nest Desktop Auth Deprecation", + "created": "2022-05-12T14:04:00.000Z", + "integrations": [ + { + "package": "nest" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/67662#issuecomment-1144425848", + "filename": "nest.markdown", + "alert_url": "https://alerts.home-assistant.io/#nest.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Haiku Firmware Update Protocol Change", + "created": "2022-04-05T00:00:00.000Z", + "integrations": [ + { + "package": "senseme" + } + ], + "filename": "senseme.markdown", + "alert_url": "https://alerts.home-assistant.io/#senseme.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "The SoChain integration is disabled due to a dependency conflict", + "created": "2022-02-01T00:00:00.000Z", + "integrations": [ + { + "package": "sochain" + } + ], + "filename": "sochain.markdown", + "alert_url": "https://alerts.home-assistant.io/#sochain.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Yeelight-manufactured Xiaomi-branded devices removed Local Control", + "created": "2021-03-29T06:00:00.000Z", + "integrations": [ + { + "package": "yeelight" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.89" + }, + "filename": "yeelight.markdown", + "alert_url": "https://alerts.home-assistant.io/#yeelight.markdown" + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_2.json b/tests/components/homeassistant_alerts/fixtures/alerts_2.json new file mode 100644 index 00000000000..2941d9da143 --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_2.json @@ -0,0 +1,159 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + { + "package": "hikvisioncam" + } + ], + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Hive shutting down North American Servers", + "created": "2021-11-13T13:58:00.000Z", + "integrations": [ + { + "package": "hive" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "2021.11.0" + }, + "filename": "hive_us.markdown", + "alert_url": "https://alerts.home-assistant.io/#hive_us.markdown" + }, + { + "title": "HomematicIP (EQ-3) blocks public IP addresses, if access to the Cloud is too frequent.", + "created": "2020-12-20T12:00:00.000Z", + "integrations": [ + { + "package": "homematicip_cloud" + } + ], + "packages": [ + { + "package": "homematicip" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.7" + }, + "filename": "homematicip_cloud.markdown", + "alert_url": "https://alerts.home-assistant.io/#homematicip_cloud.markdown" + }, + { + "title": "Logitech no longer accepting API applications", + "created": "2022-05-16T12:00:00.000Z", + "integrations": [ + { + "package": "logi_circle" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/71945", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.92.0" + }, + "filename": "logi_circle.markdown", + "alert_url": "https://alerts.home-assistant.io/#logi_circle.markdown" + }, + { + "title": "New Neato Botvacs Do Not Support Existing API", + "created": "2021-12-20T13:27:00.000Z", + "integrations": [ + { + "package": "neato" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "neato.markdown", + "alert_url": "https://alerts.home-assistant.io/#neato.markdown" + }, + { + "title": "Nest Desktop Auth Deprecation", + "created": "2022-05-12T14:04:00.000Z", + "integrations": [ + { + "package": "nest" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/67662#issuecomment-1144425848", + "filename": "nest.markdown", + "alert_url": "https://alerts.home-assistant.io/#nest.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Haiku Firmware Update Protocol Change", + "created": "2022-04-05T00:00:00.000Z", + "integrations": [ + { + "package": "senseme" + } + ], + "filename": "senseme.markdown", + "alert_url": "https://alerts.home-assistant.io/#senseme.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "The SoChain integration is disabled due to a dependency conflict", + "created": "2022-02-01T00:00:00.000Z", + "integrations": [ + { + "package": "sochain" + } + ], + "filename": "sochain.markdown", + "alert_url": "https://alerts.home-assistant.io/#sochain.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Yeelight-manufactured Xiaomi-branded devices removed Local Control", + "created": "2021-03-29T06:00:00.000Z", + "integrations": [ + { + "package": "yeelight" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.89" + }, + "filename": "yeelight.markdown", + "alert_url": "https://alerts.home-assistant.io/#yeelight.markdown" + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json new file mode 100644 index 00000000000..25ce79d7e7c --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json @@ -0,0 +1,27 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_package.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_package.json new file mode 100644 index 00000000000..bcc0a0223ee --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_no_package.json @@ -0,0 +1,33 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + {} + ], + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json new file mode 100644 index 00000000000..89f277cf69b --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json @@ -0,0 +1,34 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + { + "package": "hikvisioncam" + } + ], + "filename": "hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + } +] diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py new file mode 100644 index 00000000000..c0b6f471033 --- /dev/null +++ b/tests/components/homeassistant_alerts/test_init.py @@ -0,0 +1,433 @@ +"""Test creating repairs from alerts.""" +from __future__ import annotations + +from datetime import timedelta +import json +from unittest.mock import ANY, patch + +import pytest + +from homeassistant.components.homeassistant_alerts import DOMAIN, UPDATE_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import assert_lists_same, async_fire_time_changed, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +def stub_alert(aioclient_mock, filename): + """Stub an alert.""" + aioclient_mock.get( + f"https://alerts.home-assistant.io/alerts/{filename}", + text=f"""--- +title: Title for {filename} +--- +Content for {filename} +""", + ) + + +@pytest.mark.parametrize( + "ha_version, expected_alerts", + ( + ( + "2022.7.0", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ( + "2022.8.0", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ( + "2021.10.0", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ), +) +async def test_alerts( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, + ha_version, + expected_alerts, +) -> None: + """Test creating issues based on alerts.""" + + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=load_fixture("alerts_1.json", "homeassistant_alerts"), + ) + for alert in expected_alerts: + stub_alert(aioclient_mock, alert[0]) + + activated_components = ( + "aladdin_connect", + "darksky", + "hikvision", + "hikvisioncam", + "hive", + "homematicip_cloud", + "logi_circle", + "neato", + "nest", + "senseme", + "sochain", + ) + for domain in activated_components: + hass.config.components.add(domain) + + with patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts + ] + } + + +@pytest.mark.parametrize( + "ha_version, fixture, expected_alerts", + ( + ( + "2022.7.0", + "alerts_no_url.json", + [ + ("dark_sky.markdown", "darksky"), + ], + ), + ( + "2022.7.0", + "alerts_no_integrations.json", + [ + ("dark_sky.markdown", "darksky"), + ], + ), + ( + "2022.7.0", + "alerts_no_package.json", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ], + ), + ), +) +async def test_bad_alerts( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, + ha_version, + fixture, + expected_alerts, +) -> None: + """Test creating issues based on alerts.""" + fixture_content = load_fixture(fixture, "homeassistant_alerts") + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=fixture_content, + ) + for alert in json.loads(fixture_content): + stub_alert(aioclient_mock, alert["filename"]) + + activated_components = ( + "darksky", + "hikvision", + "hikvisioncam", + ) + for domain in activated_components: + hass.config.components.add(domain) + + with patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts + ] + } + + +async def test_no_alerts( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test creating issues based on alerts.""" + + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text="", + ) + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +@pytest.mark.parametrize( + "ha_version, fixture_1, expected_alerts_1, fixture_2, expected_alerts_2", + ( + ( + "2022.7.0", + "alerts_1.json", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + "alerts_2.json", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ( + "2022.7.0", + "alerts_2.json", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + "alerts_1.json", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ), +) +async def test_alerts_change( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, + ha_version: str, + fixture_1: str, + expected_alerts_1: list[tuple(str, str)], + fixture_2: str, + expected_alerts_2: list[tuple(str, str)], +) -> None: + """Test creating issues based on alerts.""" + fixture_1_content = load_fixture(fixture_1, "homeassistant_alerts") + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=fixture_1_content, + ) + for alert in json.loads(fixture_1_content): + stub_alert(aioclient_mock, alert["filename"]) + + activated_components = ( + "aladdin_connect", + "darksky", + "hikvision", + "hikvisioncam", + "hive", + "homematicip_cloud", + "logi_circle", + "neato", + "nest", + "senseme", + "sochain", + ) + for domain in activated_components: + hass.config.components.add(domain) + + with patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + now = dt_util.utcnow() + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert_lists_same( + msg["result"]["issues"], + [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts_1 + ], + ) + + fixture_2_content = load_fixture(fixture_2, "homeassistant_alerts") + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=fixture_2_content, + ) + for alert in json.loads(fixture_2_content): + stub_alert(aioclient_mock, alert["filename"]) + + future = now + UPDATE_INTERVAL + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert_lists_same( + msg["result"]["issues"], + [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts_2 + ], + ) From a4d4170279f712a779ea96aedd3cb2275a51a0ba Mon Sep 17 00:00:00 2001 From: Jevgeni Kiski Date: Wed, 27 Jul 2022 11:58:43 +0300 Subject: [PATCH 729/820] Add new vallox temperature and fan sensors (#75783) Co-authored-by: Franck Nijhof --- homeassistant/components/vallox/sensor.py | 77 ++++++++++++++++++----- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 48b44dd97fe..d3357e50ad2 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, + FREQUENCY_HERTZ, PERCENTAGE, TEMP_CELSIUS, ) @@ -32,7 +33,7 @@ from .const import ( ) -class ValloxSensor(ValloxEntity, SensorEntity): +class ValloxSensorEntity(ValloxEntity, SensorEntity): """Representation of a Vallox sensor.""" entity_description: ValloxSensorEntityDescription @@ -58,10 +59,17 @@ class ValloxSensor(ValloxEntity, SensorEntity): if (metric_key := self.entity_description.metric_key) is None: return None - return self.coordinator.data.get_metric(metric_key) + value = self.coordinator.data.get_metric(metric_key) + + if self.entity_description.round_ndigits is not None and isinstance( + value, float + ): + value = round(value, self.entity_description.round_ndigits) + + return value -class ValloxProfileSensor(ValloxSensor): +class ValloxProfileSensor(ValloxSensorEntity): """Child class for profile reporting.""" @property @@ -77,7 +85,7 @@ class ValloxProfileSensor(ValloxSensor): # # Therefore, first query the overall state of the device, and report zero percent fan speed in case # it is not in regular operation mode. -class ValloxFanSpeedSensor(ValloxSensor): +class ValloxFanSpeedSensor(ValloxSensorEntity): """Child class for fan speed reporting.""" @property @@ -87,7 +95,7 @@ class ValloxFanSpeedSensor(ValloxSensor): return super().native_value if fan_is_on else 0 -class ValloxFilterRemainingSensor(ValloxSensor): +class ValloxFilterRemainingSensor(ValloxSensorEntity): """Child class for filter remaining time reporting.""" @property @@ -104,7 +112,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): ) -class ValloxCellStateSensor(ValloxSensor): +class ValloxCellStateSensor(ValloxSensorEntity): """Child class for cell state reporting.""" @property @@ -123,15 +131,16 @@ class ValloxSensorEntityDescription(SensorEntityDescription): """Describes Vallox sensor entity.""" metric_key: str | None = None - sensor_type: type[ValloxSensor] = ValloxSensor + entity_type: type[ValloxSensorEntity] = ValloxSensorEntity + round_ndigits: int | None = None -SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( +SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ValloxSensorEntityDescription( key="current_profile", name="Current profile", icon="mdi:gauge", - sensor_type=ValloxProfileSensor, + entity_type=ValloxProfileSensor, ), ValloxSensorEntityDescription( key="fan_speed", @@ -140,20 +149,40 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, - sensor_type=ValloxFanSpeedSensor, + entity_type=ValloxFanSpeedSensor, + ), + ValloxSensorEntityDescription( + key="extract_fan_speed", + name="Extract fan speed", + metric_key="A_CYC_EXTR_FAN_SPEED", + icon="mdi:fan", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=FREQUENCY_HERTZ, + entity_type=ValloxFanSpeedSensor, + entity_registry_enabled_default=False, + ), + ValloxSensorEntityDescription( + key="supply_fan_speed", + name="Supply fan speed", + metric_key="A_CYC_SUPP_FAN_SPEED", + icon="mdi:fan", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=FREQUENCY_HERTZ, + entity_type=ValloxFanSpeedSensor, + entity_registry_enabled_default=False, ), ValloxSensorEntityDescription( key="remaining_time_for_filter", name="Remaining time for filter", device_class=SensorDeviceClass.TIMESTAMP, - sensor_type=ValloxFilterRemainingSensor, + entity_type=ValloxFilterRemainingSensor, ), ValloxSensorEntityDescription( key="cell_state", name="Cell state", icon="mdi:swap-horizontal-bold", metric_key="A_CYC_CELL_STATE", - sensor_type=ValloxCellStateSensor, + entity_type=ValloxCellStateSensor, ), ValloxSensorEntityDescription( key="extract_air", @@ -187,6 +216,23 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), + ValloxSensorEntityDescription( + key="supply_cell_air", + name="Supply cell air", + metric_key="A_CYC_TEMP_SUPPLY_CELL_AIR", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + ValloxSensorEntityDescription( + key="optional_air", + name="Optional air", + metric_key="A_CYC_TEMP_OPTIONAL", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + entity_registry_enabled_default=False, + ), ValloxSensorEntityDescription( key="humidity", name="Humidity", @@ -202,6 +248,8 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + round_ndigits=0, ), ValloxSensorEntityDescription( key="co2", @@ -210,6 +258,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + entity_registry_enabled_default=False, ), ) @@ -223,7 +272,7 @@ async def async_setup_entry( async_add_entities( [ - description.sensor_type(name, coordinator, description) - for description in SENSORS + description.entity_type(name, coordinator, description) + for description in SENSOR_ENTITIES ] ) From aaf5837759de59b18992bb2ce893a71def209526 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 11:29:52 +0200 Subject: [PATCH 730/820] Deprecate the Ambee integration (#75805) --- homeassistant/components/ambee/__init__.py | 22 ++++++++++++++ homeassistant/components/ambee/manifest.json | 1 + homeassistant/components/ambee/strings.json | 6 ++++ .../components/ambee/translations/en.json | 6 ++++ tests/components/ambee/test_init.py | 11 +++++++ tests/components/repairs/__init__.py | 29 +++++++++++++++++++ 6 files changed, 75 insertions(+) diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py index ee311df75fa..dbc503928c4 100644 --- a/homeassistant/components/ambee/__init__.py +++ b/homeassistant/components/ambee/__init__.py @@ -3,10 +3,16 @@ from __future__ import annotations from ambee import AirQuality, Ambee, AmbeeAuthenticationError, Pollen +from homeassistant.components.repairs import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN @@ -14,6 +20,20 @@ from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_P PLATFORMS = [Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Ambee integration.""" + async_create_issue( + hass, + DOMAIN, + "pending_removal", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ambee from a config entry.""" hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {}) @@ -67,4 +87,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + async_delete_issue(hass, DOMAIN, "pending_removal") return unload_ok diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json index 3226e9de3a3..f74832100cd 100644 --- a/homeassistant/components/ambee/manifest.json +++ b/homeassistant/components/ambee/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambee", "requirements": ["ambee==0.4.0"], + "dependencies": ["repairs"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/ambee/strings.json b/homeassistant/components/ambee/strings.json index e3c306788dd..7d0e75877c9 100644 --- a/homeassistant/components/ambee/strings.json +++ b/homeassistant/components/ambee/strings.json @@ -24,5 +24,11 @@ "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "issues": { + "pending_removal": { + "title": "The Ambee integration is being removed", + "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue." + } } } diff --git a/homeassistant/components/ambee/translations/en.json b/homeassistant/components/ambee/translations/en.json index 433580e8023..03f4c3241b6 100644 --- a/homeassistant/components/ambee/translations/en.json +++ b/homeassistant/components/ambee/translations/en.json @@ -24,5 +24,11 @@ "description": "Set up Ambee to integrate with Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue.", + "title": "The Ambee integration is being removed" + } } } \ No newline at end of file diff --git a/tests/components/ambee/test_init.py b/tests/components/ambee/test_init.py index c6ad45735ff..059c58da803 100644 --- a/tests/components/ambee/test_init.py +++ b/tests/components/ambee/test_init.py @@ -1,6 +1,8 @@ """Tests for the Ambee integration.""" +from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock, MagicMock, patch +from aiohttp import ClientWebSocketResponse from ambee import AmbeeConnectionError from ambee.exceptions import AmbeeAuthenticationError import pytest @@ -10,12 +12,14 @@ from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +from tests.components.repairs import get_repairs async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ambee: AsyncMock, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], ) -> None: """Test the Ambee configuration entry loading/unloading.""" mock_config_entry.add_to_hass(hass) @@ -24,9 +28,16 @@ async def test_load_unload_config_entry( assert mock_config_entry.state is ConfigEntryState.LOADED + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "pending_removal" + await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 0 + assert not hass.data.get(DOMAIN) diff --git a/tests/components/repairs/__init__.py b/tests/components/repairs/__init__.py index b0d26f49ee4..77971d0284b 100644 --- a/tests/components/repairs/__init__.py +++ b/tests/components/repairs/__init__.py @@ -1 +1,30 @@ """Tests for the repairs integration.""" +from collections.abc import Awaitable, Callable + +from aiohttp import ClientWebSocketResponse + +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def get_repairs( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +): + """Return the repairs list of issues.""" + assert await async_setup_component(hass, "repairs", {}) + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] + + return msg["result"]["issues"] From f8b115dd9ddeb9ff60c11a972166d466ecc92670 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 27 Jul 2022 11:28:58 +0100 Subject: [PATCH 731/820] Add xiaomi_ble voltage, consumable and formaldehyde sensors (#75807) Co-authored-by: Martin Hjelmare --- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 14 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/test_sensor.py | 148 ++++++++++++++++++ 5 files changed, 165 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 457eac60407..2e1a502ca69 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.5.1"], + "requirements": ["xiaomi-ble==0.6.1"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index f33c864fa4a..a96722620a9 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -30,7 +30,9 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONDUCTIVITY, + ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, PRESSURE_MBAR, @@ -74,6 +76,12 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( + key=str(Units.ELECTRIC_POTENTIAL_VOLT), + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), ( DeviceClass.SIGNAL_STRENGTH, Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -98,6 +106,12 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=CONDUCTIVITY, state_class=SensorStateClass.MEASUREMENT, ), + # Used for e.g. formaldehyde + (None, Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER): SensorEntityDescription( + key=str(Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER), + native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), } diff --git a/requirements_all.txt b/requirements_all.txt index f1e14dc0aeb..7240c52de1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2470,7 +2470,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.5.1 +xiaomi-ble==0.6.1 # homeassistant.components.knx xknx==0.22.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d52446c4a26..0bf0328b77e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.5.1 +xiaomi-ble==0.6.1 # homeassistant.components.knx xknx==0.22.0 diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index b95ea37311e..dca00e92254 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -50,6 +50,154 @@ async def test_sensors(hass): await hass.async_block_till_done() +async def test_xiaomi_formaldeyhde(hass): + """Make sure that formldehyde sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1010, payload len is 0x2 and payload is 0xf400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x10\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("sensor.test_device_formaldehyde") + sensor_attr = sensor.attributes + assert sensor.state == "2.44" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Formaldehyde" + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "mg/m³" + assert sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_consumable(hass): + """Make sure that consumable sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x13\x10\x02\x60\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("sensor.test_device_consumable") + sensor_attr = sensor.attributes + assert sensor.state == "96" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Consumable" + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_battery_voltage(hass): + """Make sure that battery voltage sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x0a10, payload len is 0x2 and payload is 0x6400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x0a\x10\x02\x64\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + volt_sensor = hass.states.get("sensor.test_device_voltage") + volt_sensor_attr = volt_sensor.attributes + assert volt_sensor.state == "3.1" + assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Voltage" + assert volt_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "V" + assert volt_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + bat_sensor = hass.states.get("sensor.test_device_battery") + bat_sensor_attr = bat_sensor.attributes + assert bat_sensor.state == "100" + assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert bat_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert bat_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_xiaomi_HHCCJCY01(hass): """This device has multiple advertisements before all sensors are visible. Test that this works.""" entry = MockConfigEntry( From 07a433a5164600c31581e12acc5a0dcd3d6b02d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 12:52:13 +0200 Subject: [PATCH 732/820] Deprecate the OpenALPR Local integration (#75810) * Deprecate the OpenALPR Local integration * Remove tests --- .../openalpr_local/image_processing.py | 16 ++ .../components/openalpr_local/manifest.json | 1 + .../components/openalpr_local/strings.json | 8 + .../openalpr_local/translations/en.json | 8 + tests/components/openalpr_local/__init__.py | 1 - .../openalpr_local/test_image_processing.py | 142 ------------------ 6 files changed, 33 insertions(+), 143 deletions(-) create mode 100644 homeassistant/components/openalpr_local/strings.json create mode 100644 homeassistant/components/openalpr_local/translations/en.json delete mode 100644 tests/components/openalpr_local/__init__.py delete mode 100644 tests/components/openalpr_local/test_image_processing.py diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index e24af7c7d1f..06688b3b297 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import io +import logging import re import voluptuous as vol @@ -13,6 +14,7 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, ) +from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -26,6 +28,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.async_ import run_callback_threadsafe +_LOGGER = logging.getLogger(__name__) + RE_ALPR_PLATE = re.compile(r"^plate\d*:") RE_ALPR_RESULT = re.compile(r"- (\w*)\s*confidence: (\d*.\d*)") @@ -69,6 +73,18 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the OpenALPR local platform.""" + create_issue( + hass, + "openalpr_local", + "pending_removal", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The OpenALPR Local is deprecated and will be removed in Home Assistant 2022.10" + ) command = [config[CONF_ALPR_BIN], "-c", config[CONF_REGION], "-"] confidence = config[CONF_CONFIDENCE] diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 8837d79369d..5243aa2b282 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -3,5 +3,6 @@ "name": "OpenALPR Local", "documentation": "https://www.home-assistant.io/integrations/openalpr_local", "codeowners": [], + "dependencies": ["repairs"], "iot_class": "local_push" } diff --git a/homeassistant/components/openalpr_local/strings.json b/homeassistant/components/openalpr_local/strings.json new file mode 100644 index 00000000000..b0dc80c6d06 --- /dev/null +++ b/homeassistant/components/openalpr_local/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The OpenALPR Local integration is being removed", + "description": "The OpenALPR Local integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/openalpr_local/translations/en.json b/homeassistant/components/openalpr_local/translations/en.json new file mode 100644 index 00000000000..9bc9035515b --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The OpenALPR Local integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The OpenALPR Local integration is being removed" + } + } +} \ No newline at end of file diff --git a/tests/components/openalpr_local/__init__.py b/tests/components/openalpr_local/__init__.py deleted file mode 100644 index 36b7703491f..00000000000 --- a/tests/components/openalpr_local/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the openalpr_local component.""" diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py deleted file mode 100644 index fefc42fe2ab..00000000000 --- a/tests/components/openalpr_local/test_image_processing.py +++ /dev/null @@ -1,142 +0,0 @@ -"""The tests for the openalpr local platform.""" -from unittest.mock import MagicMock, PropertyMock, patch - -import pytest - -import homeassistant.components.image_processing as ip -from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.setup import async_setup_component - -from tests.common import assert_setup_component, async_capture_events, load_fixture -from tests.components.image_processing import common - - -@pytest.fixture -async def setup_openalpr_local(hass): - """Set up openalpr local.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera", "name": "test local"}, - "region": "eu", - }, - "camera": {"platform": "demo"}, - } - - with patch( - "homeassistant.components.openalpr_local.image_processing." - "OpenAlprLocalEntity.should_poll", - new_callable=PropertyMock(return_value=False), - ): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - -@pytest.fixture -def url(hass, setup_openalpr_local): - """Return the camera URL.""" - state = hass.states.get("camera.demo_camera") - return f"{hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" - - -@pytest.fixture -async def alpr_events(hass): - """Listen for events.""" - return async_capture_events(hass, "image_processing.found_plate") - - -@pytest.fixture -def popen_mock(): - """Get a Popen mock back.""" - async_popen = MagicMock() - - async def communicate(input=None): - """Communicate mock.""" - fixture = bytes(load_fixture("alpr_stdout.txt"), "utf-8") - return (fixture, None) - - async_popen.communicate = communicate - - with patch("asyncio.create_subprocess_exec", return_value=async_popen) as mock: - yield mock - - -async def test_setup_platform(hass): - """Set up platform with one entity.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera"}, - "region": "eu", - }, - "camera": {"platform": "demo"}, - } - - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - assert hass.states.get("image_processing.openalpr_demo_camera") - - -async def test_setup_platform_name(hass): - """Set up platform with one entity and set name.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera", "name": "test local"}, - "region": "eu", - }, - "camera": {"platform": "demo"}, - } - - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - assert hass.states.get("image_processing.test_local") - - -async def test_setup_platform_without_region(hass): - """Set up platform with one entity without region.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera"}, - }, - "camera": {"platform": "demo"}, - } - - with assert_setup_component(0, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - -async def test_openalpr_process_image( - setup_openalpr_local, - url, - hass, - alpr_events, - popen_mock, - aioclient_mock, -): - """Set up and scan a picture and test plates from event.""" - aioclient_mock.get(url, content=b"image") - - common.async_scan(hass, entity_id="image_processing.test_local") - await hass.async_block_till_done() - - state = hass.states.get("image_processing.test_local") - - assert popen_mock.called - assert len(alpr_events) == 5 - assert state.attributes.get("vehicles") == 1 - assert state.state == "PE3R2X" - - event_data = [ - event.data for event in alpr_events if event.data.get("plate") == "PE3R2X" - ] - assert len(event_data) == 1 - assert event_data[0]["plate"] == "PE3R2X" - assert event_data[0]["confidence"] == float(98.9371) - assert event_data[0]["entity_id"] == "image_processing.test_local" From 699fff08edacd4349ac69d9393cdb20b0e75e7bd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 27 Jul 2022 13:21:07 +0200 Subject: [PATCH 733/820] Update frontend to 20220727.0 (#75813) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f19ef00e2b7..2a1fb2d3b37 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220707.1"], + "requirements": ["home-assistant-frontend==20220727.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3a6ae411c5e..2554f0185b4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220707.1 +home-assistant-frontend==20220727.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 7240c52de1c..ea3f2dd3e6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.1 +home-assistant-frontend==20220727.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0bf0328b77e..9f19f813747 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.1 +home-assistant-frontend==20220727.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 6254142b8a7f0becdd5953efbb49f97afc93ec34 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 27 Jul 2022 08:03:51 -0400 Subject: [PATCH 734/820] Fix ZHA on with timed off cluster command (#75815) --- homeassistant/components/zha/core/channels/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index b2870d84e15..d310157327b 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -365,7 +365,7 @@ class OnOffChannel(ZigbeeChannel): should_accept = args[0] on_time = args[1] # 0 is always accept 1 is only accept when already on - if should_accept == 0 or (should_accept == 1 and self._state): + if should_accept == 0 or (should_accept == 1 and bool(self.on_off)): if self._off_listener is not None: self._off_listener() self._off_listener = None From 314778cb50b5107bc18da04778ef9c8818352994 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 14:17:22 +0200 Subject: [PATCH 735/820] Raise YAML removal issue for Anthem A/V Receivers (#75816) --- homeassistant/components/anthemav/manifest.json | 1 + homeassistant/components/anthemav/media_player.py | 15 ++++++++++++++- homeassistant/components/anthemav/strings.json | 6 ++++++ .../components/anthemav/translations/en.json | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 33fe565ad03..27db9df32a3 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -3,6 +3,7 @@ "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", "requirements": ["anthemav==1.3.2"], + "dependencies": ["repairs"], "codeowners": ["@hyralex"], "config_flow": true, "iot_class": "local_push", diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 93fdedef054..3c3a363a6db 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -55,8 +56,20 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up our socket to the AVR.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( - "AnthemAV configuration is deprecated and has been automatically imported. Please remove the integration from your configuration file" + "Configuration of the Anthem A/V Receivers integration in YAML is " + "deprecated and will be removed in Home Assistant 2022.10; Your " + "existing configuration has been imported into the UI automatically " + "and can be safely removed from your configuration.yaml file" ) await hass.config_entries.flow.async_init( DOMAIN, diff --git a/homeassistant/components/anthemav/strings.json b/homeassistant/components/anthemav/strings.json index 1f1dd0ec75b..b4e777c4de1 100644 --- a/homeassistant/components/anthemav/strings.json +++ b/homeassistant/components/anthemav/strings.json @@ -15,5 +15,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Anthem A/V Receivers YAML configuration is being removed", + "description": "Configuring Anthem A/V Receivers using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Anthem A/V Receivers YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/anthemav/translations/en.json b/homeassistant/components/anthemav/translations/en.json index 9177d5a6e70..af4c83eb2a8 100644 --- a/homeassistant/components/anthemav/translations/en.json +++ b/homeassistant/components/anthemav/translations/en.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Anthem A/V Receivers using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Anthem A/V Receivers YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Anthem A/V Receivers YAML configuration is being removed" + } } } \ No newline at end of file From 49854b809cc8ce10e7e18ad17a8cffd541555ed7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 27 Jul 2022 14:17:38 +0200 Subject: [PATCH 736/820] Netatmo entity renaming and clean up (#75337) --- homeassistant/components/netatmo/camera.py | 12 +-- homeassistant/components/netatmo/climate.py | 20 ++--- homeassistant/components/netatmo/const.py | 66 +++++++------- .../components/netatmo/data_handler.py | 86 +++++++++---------- homeassistant/components/netatmo/light.py | 7 +- .../components/netatmo/netatmo_entity_base.py | 20 +++-- homeassistant/components/netatmo/select.py | 4 +- homeassistant/components/netatmo/sensor.py | 23 +++-- tests/components/netatmo/test_camera.py | 26 +++--- tests/components/netatmo/test_light.py | 2 +- tests/components/netatmo/test_sensor.py | 6 +- 11 files changed, 128 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index e5eaad50c9f..3235d16479c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -112,6 +112,8 @@ async def async_setup_entry( class NetatmoCamera(NetatmoBase, Camera): """Representation of a Netatmo camera.""" + _attr_brand = MANUFACTURER + _attr_has_entity_name = True _attr_supported_features = CameraEntityFeature.STREAM def __init__( @@ -126,14 +128,13 @@ class NetatmoCamera(NetatmoBase, Camera): Camera.__init__(self) super().__init__(data_handler) - self._data_classes.append( + self._publishers.append( {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} ) self._id = camera_id self._home_id = home_id self._device_name = self._data.get_camera(camera_id=camera_id)["name"] - self._attr_name = f"{MANUFACTURER} {self._device_name}" self._model = camera_type self._netatmo_type = TYPE_SECURITY self._attr_unique_id = f"{self._id}-{self._model}" @@ -193,7 +194,7 @@ class NetatmoCamera(NetatmoBase, Camera): """Return data for this entity.""" return cast( pyatmo.AsyncCameraData, - self.data_handler.data[self._data_classes[0]["name"]], + self.data_handler.data[self._publishers[0]["name"]], ) async def async_camera_image( @@ -219,11 +220,6 @@ class NetatmoCamera(NetatmoBase, Camera): """Return True if entity is available.""" return bool(self._alim_status == "on" or self._status == "disconnected") - @property - def brand(self) -> str: - """Return the camera brand.""" - return MANUFACTURER - @property def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index cc515e50a11..eb7d996eefb 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -127,7 +127,7 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( + await data_handler.subscribe( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) @@ -185,14 +185,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._room = room self._id = self._room.entity_id - self._climate_state_class = ( - f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" - ) - self._climate_state: pyatmo.AsyncClimate = data_handler.data[ - self._climate_state_class - ] + self._signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" + self._climate_state: pyatmo.AsyncClimate = data_handler.data[self._signal_name] - self._data_classes.extend( + self._publishers.extend( [ { "name": CLIMATE_TOPOLOGY_CLASS_NAME, @@ -201,7 +197,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): { "name": CLIMATE_STATE_CLASS_NAME, "home_id": self._room.home.entity_id, - SIGNAL_NAME: self._climate_state_class, + SIGNAL_NAME: self._signal_name, }, ] ) @@ -254,7 +250,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.data_handler, module, self._id, - self._climate_state_class, + self._signal_name, ), ) @@ -278,7 +274,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ATTR_SELECTED_SCHEDULE ] = self._selected_schedule self.async_write_ha_state() - self.data_handler.async_force_update(self._climate_state_class) + self.data_handler.async_force_update(self._signal_name) return home = data["home"] @@ -295,7 +291,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._attr_target_temperature = self._away_temperature elif self._attr_preset_mode == PRESET_SCHEDULE: self.async_update_callback() - self.data_handler.async_force_update(self._climate_state_class) + self.data_handler.async_force_update(self._signal_name) self.async_write_ha_state() return diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 5fda8759540..6bd66fa9644 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -73,16 +73,16 @@ DATA_HANDLER = "netatmo_data_handler" SIGNAL_NAME = "signal_name" NETATMO_CREATE_BATTERY = "netatmo_create_battery" -CONF_CLOUDHOOK_URL = "cloudhook_url" -CONF_WEATHER_AREAS = "weather_areas" -CONF_NEW_AREA = "new_area" CONF_AREA_NAME = "area_name" +CONF_CLOUDHOOK_URL = "cloudhook_url" CONF_LAT_NE = "lat_ne" -CONF_LON_NE = "lon_ne" CONF_LAT_SW = "lat_sw" +CONF_LON_NE = "lon_ne" CONF_LON_SW = "lon_sw" +CONF_NEW_AREA = "new_area" CONF_PUBLIC_MODE = "mode" CONF_UUID = "uuid" +CONF_WEATHER_AREAS = "weather_areas" OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" @@ -94,53 +94,53 @@ DATA_HOMES = "netatmo_homes" DATA_PERSONS = "netatmo_persons" DATA_SCHEDULES = "netatmo_schedules" -NETATMO_WEBHOOK_URL = None NETATMO_EVENT = "netatmo_event" +NETATMO_WEBHOOK_URL = None -DEFAULT_PERSON = "unknown" DEFAULT_DISCOVERY = True +DEFAULT_PERSON = "unknown" DEFAULT_WEBHOOKS = False -ATTR_PSEUDO = "pseudo" +ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" ATTR_EVENT_TYPE = "event_type" +ATTR_FACE_URL = "face_url" ATTR_HEATING_POWER_REQUEST = "heating_power_request" ATTR_HOME_ID = "home_id" ATTR_HOME_NAME = "home_name" +ATTR_IS_KNOWN = "is_known" ATTR_PERSON = "person" ATTR_PERSONS = "persons" -ATTR_IS_KNOWN = "is_known" -ATTR_FACE_URL = "face_url" +ATTR_PSEUDO = "pseudo" ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_NAME = "schedule_name" ATTR_SELECTED_SCHEDULE = "selected_schedule" -ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" SERVICE_SET_CAMERA_LIGHT = "set_camera_light" -SERVICE_SET_SCHEDULE = "set_schedule" -SERVICE_SET_PERSONS_HOME = "set_persons_home" SERVICE_SET_PERSON_AWAY = "set_person_away" +SERVICE_SET_PERSONS_HOME = "set_persons_home" +SERVICE_SET_SCHEDULE = "set_schedule" # Climate events -EVENT_TYPE_SET_POINT = "set_point" EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" -EVENT_TYPE_THERM_MODE = "therm_mode" EVENT_TYPE_SCHEDULE = "schedule" +EVENT_TYPE_SET_POINT = "set_point" +EVENT_TYPE_THERM_MODE = "therm_mode" # Camera events -EVENT_TYPE_LIGHT_MODE = "light_mode" -EVENT_TYPE_CAMERA_OUTDOOR = "outdoor" EVENT_TYPE_CAMERA_ANIMAL = "animal" EVENT_TYPE_CAMERA_HUMAN = "human" -EVENT_TYPE_CAMERA_VEHICLE = "vehicle" EVENT_TYPE_CAMERA_MOVEMENT = "movement" +EVENT_TYPE_CAMERA_OUTDOOR = "outdoor" EVENT_TYPE_CAMERA_PERSON = "person" EVENT_TYPE_CAMERA_PERSON_AWAY = "person_away" +EVENT_TYPE_CAMERA_VEHICLE = "vehicle" +EVENT_TYPE_LIGHT_MODE = "light_mode" # Door tags -EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move" +EVENT_TYPE_ALARM_STARTED = "alarm_started" EVENT_TYPE_DOOR_TAG_BIG_MOVE = "tag_big_move" EVENT_TYPE_DOOR_TAG_OPEN = "tag_open" +EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move" EVENT_TYPE_OFF = "off" EVENT_TYPE_ON = "on" -EVENT_TYPE_ALARM_STARTED = "alarm_started" OUTDOOR_CAMERA_TRIGGERS = [ EVENT_TYPE_CAMERA_ANIMAL, @@ -149,46 +149,46 @@ OUTDOOR_CAMERA_TRIGGERS = [ EVENT_TYPE_CAMERA_VEHICLE, ] INDOOR_CAMERA_TRIGGERS = [ - EVENT_TYPE_CAMERA_MOVEMENT, - EVENT_TYPE_CAMERA_PERSON, - EVENT_TYPE_CAMERA_PERSON_AWAY, EVENT_TYPE_ALARM_STARTED, + EVENT_TYPE_CAMERA_MOVEMENT, + EVENT_TYPE_CAMERA_PERSON_AWAY, + EVENT_TYPE_CAMERA_PERSON, ] DOOR_TAG_TRIGGERS = [ - EVENT_TYPE_DOOR_TAG_SMALL_MOVE, EVENT_TYPE_DOOR_TAG_BIG_MOVE, EVENT_TYPE_DOOR_TAG_OPEN, + EVENT_TYPE_DOOR_TAG_SMALL_MOVE, ] CLIMATE_TRIGGERS = [ - EVENT_TYPE_SET_POINT, EVENT_TYPE_CANCEL_SET_POINT, + EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, ] EVENT_ID_MAP = { - EVENT_TYPE_CAMERA_MOVEMENT: "device_id", - EVENT_TYPE_CAMERA_PERSON: "device_id", - EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id", + EVENT_TYPE_ALARM_STARTED: "device_id", EVENT_TYPE_CAMERA_ANIMAL: "device_id", EVENT_TYPE_CAMERA_HUMAN: "device_id", + EVENT_TYPE_CAMERA_MOVEMENT: "device_id", EVENT_TYPE_CAMERA_OUTDOOR: "device_id", + EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id", + EVENT_TYPE_CAMERA_PERSON: "device_id", EVENT_TYPE_CAMERA_VEHICLE: "device_id", - EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id", + EVENT_TYPE_CANCEL_SET_POINT: "room_id", EVENT_TYPE_DOOR_TAG_BIG_MOVE: "device_id", EVENT_TYPE_DOOR_TAG_OPEN: "device_id", + EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id", EVENT_TYPE_LIGHT_MODE: "device_id", - EVENT_TYPE_ALARM_STARTED: "device_id", - EVENT_TYPE_CANCEL_SET_POINT: "room_id", EVENT_TYPE_SET_POINT: "room_id", EVENT_TYPE_THERM_MODE: "home_id", } -MODE_LIGHT_ON = "on" -MODE_LIGHT_OFF = "off" MODE_LIGHT_AUTO = "auto" +MODE_LIGHT_OFF = "off" +MODE_LIGHT_ON = "on" CAMERA_LIGHT_MODES = [MODE_LIGHT_ON, MODE_LIGHT_OFF, MODE_LIGHT_AUTO] WEBHOOK_ACTIVATION = "webhook_activation" WEBHOOK_DEACTIVATION = "webhook_deactivation" +WEBHOOK_LIGHT_MODE = "NOC-light_mode" WEBHOOK_NACAMERA_CONNECTION = "NACamera-connection" WEBHOOK_PUSH_TYPE = "push_type" -WEBHOOK_LIGHT_MODE = "NOC-light_mode" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 1d6345506c1..3a1ea73e311 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -64,11 +64,11 @@ class NetatmoDevice: data_handler: NetatmoDataHandler device: pyatmo.climate.NetatmoModule parent_id: str - state_class_name: str + signal_name: str @dataclass -class NetatmoDataClass: +class NetatmoPublisher: """Class for keeping track of Netatmo data class metadata.""" name: str @@ -85,7 +85,7 @@ class NetatmoDataHandler: self.hass = hass self.config_entry = config_entry self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH] - self.data_classes: dict = {} + self.publisher: dict[str, NetatmoPublisher] = {} self.data: dict = {} self._queue: deque = deque() self._webhook: bool = False @@ -107,7 +107,7 @@ class NetatmoDataHandler: await asyncio.gather( *[ - self.register_data_class(data_class, data_class, None) + self.subscribe(data_class, data_class, None) for data_class in ( CLIMATE_TOPOLOGY_CLASS_NAME, CAMERA_DATA_CLASS_NAME, @@ -128,20 +128,18 @@ class NetatmoDataHandler: if data_class.next_scan > time(): continue - if data_class_name := data_class.name: - self.data_classes[data_class_name].next_scan = ( - time() + data_class.interval - ) + if publisher := data_class.name: + self.publisher[publisher].next_scan = time() + data_class.interval - await self.async_fetch_data(data_class_name) + await self.async_fetch_data(publisher) self._queue.rotate(BATCH_SIZE) @callback - def async_force_update(self, data_class_entry: str) -> None: + def async_force_update(self, signal_name: str) -> None: """Prioritize data retrieval for given data class entry.""" - self.data_classes[data_class_entry].next_scan = time() - self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) + self.publisher[signal_name].next_scan = time() + self._queue.rotate(-(self._queue.index(self.publisher[signal_name]))) async def handle_event(self, event: dict) -> None: """Handle webhook events.""" @@ -157,17 +155,17 @@ class NetatmoDataHandler: _LOGGER.debug("%s camera reconnected", MANUFACTURER) self.async_force_update(CAMERA_DATA_CLASS_NAME) - async def async_fetch_data(self, data_class_entry: str) -> None: + async def async_fetch_data(self, signal_name: str) -> None: """Fetch data and notify.""" - if self.data[data_class_entry] is None: + if self.data[signal_name] is None: return try: - await self.data[data_class_entry].async_update() + await self.data[signal_name].async_update() except pyatmo.NoDevice as err: _LOGGER.debug(err) - self.data[data_class_entry] = None + self.data[signal_name] = None except pyatmo.ApiError as err: _LOGGER.debug(err) @@ -176,56 +174,52 @@ class NetatmoDataHandler: _LOGGER.debug(err) return - for update_callback in self.data_classes[data_class_entry].subscriptions: + for update_callback in self.publisher[signal_name].subscriptions: if update_callback: update_callback() - async def register_data_class( + async def subscribe( self, - data_class_name: str, - data_class_entry: str, + publisher: str, + signal_name: str, update_callback: CALLBACK_TYPE | None, **kwargs: Any, ) -> None: - """Register data class.""" - if data_class_entry in self.data_classes: - if update_callback not in self.data_classes[data_class_entry].subscriptions: - self.data_classes[data_class_entry].subscriptions.append( - update_callback - ) + """Subscribe to publisher.""" + if signal_name in self.publisher: + if update_callback not in self.publisher[signal_name].subscriptions: + self.publisher[signal_name].subscriptions.append(update_callback) return - self.data_classes[data_class_entry] = NetatmoDataClass( - name=data_class_entry, - interval=DEFAULT_INTERVALS[data_class_name], - next_scan=time() + DEFAULT_INTERVALS[data_class_name], + self.publisher[signal_name] = NetatmoPublisher( + name=signal_name, + interval=DEFAULT_INTERVALS[publisher], + next_scan=time() + DEFAULT_INTERVALS[publisher], subscriptions=[update_callback], ) - self.data[data_class_entry] = DATA_CLASSES[data_class_name]( - self._auth, **kwargs - ) + self.data[signal_name] = DATA_CLASSES[publisher](self._auth, **kwargs) try: - await self.async_fetch_data(data_class_entry) + await self.async_fetch_data(signal_name) except KeyError: - self.data_classes.pop(data_class_entry) + self.publisher.pop(signal_name) raise - self._queue.append(self.data_classes[data_class_entry]) - _LOGGER.debug("Data class %s added", data_class_entry) + self._queue.append(self.publisher[signal_name]) + _LOGGER.debug("Publisher %s added", signal_name) - async def unregister_data_class( - self, data_class_entry: str, update_callback: CALLBACK_TYPE | None + async def unsubscribe( + self, signal_name: str, update_callback: CALLBACK_TYPE | None ) -> None: - """Unregister data class.""" - self.data_classes[data_class_entry].subscriptions.remove(update_callback) + """Unsubscribe from publisher.""" + self.publisher[signal_name].subscriptions.remove(update_callback) - if not self.data_classes[data_class_entry].subscriptions: - self._queue.remove(self.data_classes[data_class_entry]) - self.data_classes.pop(data_class_entry) - self.data.pop(data_class_entry) - _LOGGER.debug("Data class %s removed", data_class_entry) + if not self.publisher[signal_name].subscriptions: + self._queue.remove(self.publisher[signal_name]) + self.publisher.pop(signal_name) + self.data.pop(signal_name) + _LOGGER.debug("Publisher %s removed", signal_name) @property def webhook(self) -> bool: diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 7e078153a8a..6567ae770f2 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -17,7 +17,6 @@ from .const import ( DATA_HANDLER, DOMAIN, EVENT_TYPE_LIGHT_MODE, - MANUFACTURER, SIGNAL_NAME, TYPE_SECURITY, WEBHOOK_LIGHT_MODE, @@ -63,6 +62,7 @@ class NetatmoLight(NetatmoBase, LightEntity): """Representation of a Netatmo Presence camera light.""" _attr_color_mode = ColorMode.ONOFF + _attr_has_entity_name = True _attr_supported_color_modes = {ColorMode.ONOFF} def __init__( @@ -76,7 +76,7 @@ class NetatmoLight(NetatmoBase, LightEntity): LightEntity.__init__(self) super().__init__(data_handler) - self._data_classes.append( + self._publishers.append( {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} ) self._id = camera_id @@ -84,7 +84,6 @@ class NetatmoLight(NetatmoBase, LightEntity): self._model = camera_type self._netatmo_type = TYPE_SECURITY self._device_name: str = self._data.get_camera(camera_id)["name"] - self._attr_name = f"{MANUFACTURER} {self._device_name}" self._is_on = False self._attr_unique_id = f"{self._id}-light" @@ -123,7 +122,7 @@ class NetatmoLight(NetatmoBase, LightEntity): """Return data for this entity.""" return cast( pyatmo.AsyncCameraData, - self.data_handler.data[self._data_classes[0]["name"]], + self.data_handler.data[self._publishers[0]["name"]], ) @property diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index decedbbdfbd..e8a346ccd84 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -1,6 +1,8 @@ """Base class for Netatmo entities.""" from __future__ import annotations +from typing import Any + from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers import device_registry as dr @@ -23,7 +25,7 @@ class NetatmoBase(Entity): def __init__(self, data_handler: NetatmoDataHandler) -> None: """Set up Netatmo entity base.""" self.data_handler = data_handler - self._data_classes: list[dict] = [] + self._publishers: list[dict[str, Any]] = [] self._device_name: str = "" self._id: str = "" @@ -35,11 +37,11 @@ class NetatmoBase(Entity): async def async_added_to_hass(self) -> None: """Entity created.""" - for data_class in self._data_classes: + for data_class in self._publishers: signal_name = data_class[SIGNAL_NAME] if "home_id" in data_class: - await self.data_handler.register_data_class( + await self.data_handler.subscribe( data_class["name"], signal_name, self.async_update_callback, @@ -47,7 +49,7 @@ class NetatmoBase(Entity): ) elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME: - await self.data_handler.register_data_class( + await self.data_handler.subscribe( data_class["name"], signal_name, self.async_update_callback, @@ -58,13 +60,13 @@ class NetatmoBase(Entity): ) else: - await self.data_handler.register_data_class( + await self.data_handler.subscribe( data_class["name"], signal_name, self.async_update_callback ) - for sub in self.data_handler.data_classes[signal_name].subscriptions: + for sub in self.data_handler.publisher[signal_name].subscriptions: if sub is None: - await self.data_handler.unregister_data_class(signal_name, None) + await self.data_handler.unsubscribe(signal_name, None) registry = dr.async_get(self.hass) if device := registry.async_get_device({(DOMAIN, self._id)}): @@ -76,8 +78,8 @@ class NetatmoBase(Entity): """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() - for data_class in self._data_classes: - await self.data_handler.unregister_data_class( + for data_class in self._publishers: + await self.data_handler.unsubscribe( data_class[SIGNAL_NAME], self.async_update_callback ) diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 56f33b04432..62e6ef25969 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -46,7 +46,7 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( + await data_handler.subscribe( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) @@ -92,7 +92,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._home = self._climate_state.homes[self._home_id] - self._data_classes.extend( + self._publishers.extend( [ { "name": CLIMATE_TOPOLOGY_CLASS_NAME, diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 217b2146cc9..bec9af96442 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -41,7 +41,6 @@ from .const import ( CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, - MANUFACTURER, NETATMO_CREATE_BATTERY, SIGNAL_NAME, TYPE_WEATHER, @@ -422,7 +421,7 @@ async def async_setup_entry( ) continue - await data_handler.register_data_class( + await data_handler.subscribe( PUBLICDATA_DATA_CLASS_NAME, signal_name, None, @@ -487,9 +486,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): super().__init__(data_handler) self.entity_description = description - self._data_classes.append( - {"name": data_class_name, SIGNAL_NAME: data_class_name} - ) + self._publishers.append({"name": data_class_name, SIGNAL_NAME: data_class_name}) self._id = module_info["_id"] self._station_id = module_info.get("main_device", self._id) @@ -507,7 +504,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): f"{module_info.get('module_name', device['type'])}" ) - self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" + self._attr_name = f"{self._device_name} {description.name}" self._model = device["type"] self._netatmo_type = TYPE_WEATHER self._attr_unique_id = f"{self._id}-{description.key}" @@ -517,7 +514,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): """Return data for this entity.""" return cast( pyatmo.AsyncWeatherStationData, - self.data_handler.data[self._data_classes[0]["name"]], + self.data_handler.data[self._publishers[0]["name"]], ) @property @@ -598,7 +595,7 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): self._id = netatmo_device.parent_id self._attr_name = f"{self._module.name} {self.entity_description.name}" - self._state_class_name = netatmo_device.state_class_name + self._signal_name = netatmo_device.signal_name self._room_id = self._module.room_id self._model = getattr(self._module.device_type, "value") @@ -734,7 +731,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - self._data_classes.append( + self._publishers.append( { "name": PUBLICDATA_DATA_CLASS_NAME, "lat_ne": area.lat_ne, @@ -751,7 +748,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._area_name = area.area_name self._id = self._area_name self._device_name = f"{self._area_name}" - self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" + self._attr_name = f"{self._device_name} {description.name}" self._show_on_map = area.show_on_map self._attr_unique_id = ( f"{self._device_name.replace(' ', '-')}-{description.key}" @@ -788,13 +785,13 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): if self.area == area: return - await self.data_handler.unregister_data_class( + await self.data_handler.unsubscribe( self._signal_name, self.async_update_callback ) self.area = area self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - self._data_classes = [ + self._publishers = [ { "name": PUBLICDATA_DATA_CLASS_NAME, "lat_ne": area.lat_ne, @@ -807,7 +804,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): ] self._mode = area.mode self._show_on_map = area.show_on_map - await self.data_handler.register_data_class( + await self.data_handler.subscribe( PUBLICDATA_DATA_CLASS_NAME, self._signal_name, self.async_update_callback, diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 2eaf713e8ee..0e10ce92288 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -32,8 +32,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): webhook_id = config_entry.data[CONF_WEBHOOK_ID] await hass.async_block_till_done() - camera_entity_indoor = "camera.netatmo_hall" - camera_entity_outdoor = "camera.netatmo_garden" + camera_entity_indoor = "camera.hall" + camera_entity_outdoor = "camera.garden" assert hass.states.get(camera_entity_indoor).state == "streaming" response = { "event_type": "off", @@ -95,7 +95,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( - "camera", "turn_off", service_data={"entity_id": "camera.netatmo_hall"} + "camera", "turn_off", service_data={"entity_id": "camera.hall"} ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( @@ -106,7 +106,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( - "camera", "turn_on", service_data={"entity_id": "camera.netatmo_hall"} + "camera", "turn_on", service_data={"entity_id": "camera.hall"} ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( @@ -130,7 +130,7 @@ async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_aut uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" stream_uri = uri + "/live/files/high/index.m3u8" - camera_entity_indoor = "camera.netatmo_hall" + camera_entity_indoor = "camera.hall" cam = hass.states.get(camera_entity_indoor) assert cam is not None @@ -161,7 +161,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) "6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,," ) stream_uri = uri + "/live/files/high/index.m3u8" - camera_entity_indoor = "camera.netatmo_garden" + camera_entity_indoor = "camera.garden" cam = hass.states.get(camera_entity_indoor) assert cam is not None @@ -188,7 +188,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "person": "Richard Doe", } @@ -205,7 +205,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth): ) data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", } with patch( @@ -231,7 +231,7 @@ async def test_service_set_person_away_invalid_person(hass, config_entry, netatm await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "person": "Batman", } @@ -259,7 +259,7 @@ async def test_service_set_persons_home_invalid_person( await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "persons": "Batman", } @@ -285,7 +285,7 @@ async def test_service_set_persons_home(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "persons": "John Doe", } @@ -312,7 +312,7 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_garden", + "entity_id": "camera.garden", "camera_light_mode": "on", } @@ -485,7 +485,7 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - camera_entity_indoor = "camera.netatmo_hall" + camera_entity_indoor = "camera.hall" with pytest.raises(Exception) as excinfo: await camera.async_get_image(hass, camera_entity_indoor) diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index a0992e7ea2c..433841f3878 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -27,7 +27,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth): await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await hass.async_block_till_done() - light_entity = "light.netatmo_garden" + light_entity = "light.garden" assert hass.states.get(light_entity).state == "unavailable" # Trigger light mode change diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index b1b5b11265a..9adc7423bd6 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -17,7 +17,7 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth): await hass.async_block_till_done() - prefix = "sensor.netatmo_mystation_" + prefix = "sensor.mystation_" assert hass.states.get(f"{prefix}temperature").state == "24.6" assert hass.states.get(f"{prefix}humidity").state == "36" @@ -34,13 +34,13 @@ async def test_public_weather_sensor(hass, config_entry, netatmo_auth): assert len(hass.states.async_all()) > 0 - prefix = "sensor.netatmo_home_max_" + prefix = "sensor.home_max_" assert hass.states.get(f"{prefix}temperature").state == "27.4" assert hass.states.get(f"{prefix}humidity").state == "76" assert hass.states.get(f"{prefix}pressure").state == "1014.4" - prefix = "sensor.netatmo_home_avg_" + prefix = "sensor.home_avg_" assert hass.states.get(f"{prefix}temperature").state == "22.7" assert hass.states.get(f"{prefix}humidity").state == "63.2" From 9dc05448350d3df14bfed51799d997db4bdb2a41 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 14:41:00 +0200 Subject: [PATCH 737/820] Raise YAML removal issue for Bose SoundTouch (#75817) --- .../components/soundtouch/manifest.json | 1 + .../components/soundtouch/media_player.py | 18 ++++++++++++++---- .../components/soundtouch/strings.json | 6 ++++++ .../components/soundtouch/translations/en.json | 6 ++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index c1c2abd3b80..4512f3a8f9b 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": ["libsoundtouch==0.8"], "zeroconf": ["_soundtouch._tcp.local."], + "dependencies": ["repairs"], "codeowners": ["@kroimon"], "iot_class": "local_polling", "loggers": ["libsoundtouch"], diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 2ed3dd9beea..74d89404d27 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -19,6 +19,7 @@ from homeassistant.components.media_player import ( from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -70,11 +71,20 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Bose SoundTouch platform.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( - "Configuration of the Bose SoundTouch platform in YAML is deprecated and will be " - "removed in a future release; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" + "Configuration of the Bose SoundTouch integration in YAML is " + "deprecated and will be removed in Home Assistant 2022.10; Your " + "existing configuration has been imported into the UI automatically " + "and can be safely removed from your configuration.yaml file" ) hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/soundtouch/strings.json b/homeassistant/components/soundtouch/strings.json index 7ebcd4c5285..6a8896c8f56 100644 --- a/homeassistant/components/soundtouch/strings.json +++ b/homeassistant/components/soundtouch/strings.json @@ -17,5 +17,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Bose SoundTouch YAML configuration is being removed", + "description": "Configuring Bose SoundTouch using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Bose SoundTouch YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/soundtouch/translations/en.json b/homeassistant/components/soundtouch/translations/en.json index 2e025d3f187..28eb32b7d43 100644 --- a/homeassistant/components/soundtouch/translations/en.json +++ b/homeassistant/components/soundtouch/translations/en.json @@ -17,5 +17,11 @@ "title": "Confirm adding Bose SoundTouch device" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Bose SoundTouch using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Bose SoundTouch YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Bose SoundTouch YAML configuration is being removed" + } } } \ No newline at end of file From 8ffdbfc462350bf1111a40523086dd83b13a057f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 15:10:29 +0200 Subject: [PATCH 738/820] Bumped version to 2022.8.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 10523aa6d53..02896fcbe96 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 48d12964414..5306a589863 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0.dev0" +version = "2022.8.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 15f87ca0a199856ed345b6bc926561fa75092450 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 28 Jul 2022 00:25:05 +0000 Subject: [PATCH 739/820] [ci skip] Translation update --- .../components/ambee/translations/de.json | 6 ++++++ .../components/ambee/translations/it.json | 6 ++++++ .../components/ambee/translations/pl.json | 6 ++++++ .../components/ambee/translations/pt-BR.json | 6 ++++++ .../ambee/translations/zh-Hant.json | 6 ++++++ .../components/anthemav/translations/de.json | 6 ++++++ .../components/anthemav/translations/it.json | 6 ++++++ .../anthemav/translations/pt-BR.json | 6 ++++++ .../anthemav/translations/zh-Hant.json | 6 ++++++ .../components/google/translations/it.json | 2 +- .../google/translations/zh-Hant.json | 2 +- .../homeassistant_alerts/translations/de.json | 8 ++++++++ .../homeassistant_alerts/translations/en.json | 8 ++++++++ .../homeassistant_alerts/translations/it.json | 8 ++++++++ .../homeassistant_alerts/translations/pl.json | 8 ++++++++ .../translations/pt-BR.json | 8 ++++++++ .../translations/zh-Hant.json | 8 ++++++++ .../lacrosse_view/translations/de.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/en.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/it.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/pt-BR.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/zh-Hant.json | 20 +++++++++++++++++++ .../components/lyric/translations/de.json | 6 ++++++ .../components/lyric/translations/pt-BR.json | 6 ++++++ .../lyric/translations/zh-Hant.json | 6 ++++++ .../components/mitemp_bt/translations/de.json | 2 +- .../components/mitemp_bt/translations/it.json | 8 ++++++++ .../mitemp_bt/translations/pt-BR.json | 2 +- .../openalpr_local/translations/de.json | 8 ++++++++ .../openalpr_local/translations/it.json | 8 ++++++++ .../openalpr_local/translations/pl.json | 8 ++++++++ .../openalpr_local/translations/pt-BR.json | 8 ++++++++ .../openalpr_local/translations/zh-Hant.json | 8 ++++++++ .../radiotherm/translations/it.json | 3 ++- .../components/senz/translations/de.json | 6 ++++++ .../components/senz/translations/pt-BR.json | 6 ++++++ .../components/senz/translations/zh-Hant.json | 6 ++++++ .../soundtouch/translations/de.json | 6 ++++++ .../soundtouch/translations/it.json | 6 ++++++ .../soundtouch/translations/pt-BR.json | 6 ++++++ .../soundtouch/translations/zh-Hant.json | 6 ++++++ .../spotify/translations/zh-Hant.json | 4 ++-- .../steam_online/translations/zh-Hant.json | 4 ++-- .../uscis/translations/zh-Hant.json | 4 ++-- .../components/xbox/translations/it.json | 6 ++++++ .../components/xbox/translations/pt-BR.json | 6 ++++++ .../components/zha/translations/de.json | 2 ++ .../components/zha/translations/pl.json | 2 ++ .../components/zha/translations/pt-BR.json | 2 ++ .../components/zha/translations/zh-Hant.json | 2 ++ 50 files changed, 342 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/de.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/en.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/it.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/pl.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/pt-BR.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/zh-Hant.json create mode 100644 homeassistant/components/lacrosse_view/translations/de.json create mode 100644 homeassistant/components/lacrosse_view/translations/en.json create mode 100644 homeassistant/components/lacrosse_view/translations/it.json create mode 100644 homeassistant/components/lacrosse_view/translations/pt-BR.json create mode 100644 homeassistant/components/lacrosse_view/translations/zh-Hant.json create mode 100644 homeassistant/components/mitemp_bt/translations/it.json create mode 100644 homeassistant/components/openalpr_local/translations/de.json create mode 100644 homeassistant/components/openalpr_local/translations/it.json create mode 100644 homeassistant/components/openalpr_local/translations/pl.json create mode 100644 homeassistant/components/openalpr_local/translations/pt-BR.json create mode 100644 homeassistant/components/openalpr_local/translations/zh-Hant.json diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json index 4359ab72349..8055ef5210f 100644 --- a/homeassistant/components/ambee/translations/de.json +++ b/homeassistant/components/ambee/translations/de.json @@ -24,5 +24,11 @@ "description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein." } } + }, + "issues": { + "pending_removal": { + "description": "Die Ambee-Integration ist dabei, aus Home Assistant entfernt zu werden und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil Ambee seine kostenlosen (begrenzten) Konten entfernt hat und keine M\u00f6glichkeit mehr f\u00fcr regul\u00e4re Nutzer bietet, sich f\u00fcr einen kostenpflichtigen Plan anzumelden.\n\nEntferne den Ambee-Integrationseintrag aus deiner Instanz, um dieses Problem zu beheben.", + "title": "Die Ambee-Integration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json index fe97ce33686..db330a9b239 100644 --- a/homeassistant/components/ambee/translations/it.json +++ b/homeassistant/components/ambee/translations/it.json @@ -24,5 +24,11 @@ "description": "Configura Ambee per l'integrazione con Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.", + "title": "L'integrazione Ambee verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pl.json b/homeassistant/components/ambee/translations/pl.json index d0b2225cc9a..255d402175d 100644 --- a/homeassistant/components/ambee/translations/pl.json +++ b/homeassistant/components/ambee/translations/pl.json @@ -24,5 +24,11 @@ "description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem." } } + }, + "issues": { + "pending_removal": { + "description": "Integracja Ambee oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c Ambee usun\u0105\u0142 ich bezp\u0142atne (ograniczone) konta i nie zapewnia ju\u017c zwyk\u0142ym u\u017cytkownikom mo\u017cliwo\u015bci zarejestrowania si\u0119 w p\u0142atnym planie. \n\nUsu\u0144 integracj\u0119 Ambee z Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Ambee zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt-BR.json b/homeassistant/components/ambee/translations/pt-BR.json index 2d960e17df2..3220de5104e 100644 --- a/homeassistant/components/ambee/translations/pt-BR.json +++ b/homeassistant/components/ambee/translations/pt-BR.json @@ -24,5 +24,11 @@ "description": "Configure o Ambee para integrar com o Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do Ambee est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, porque a Ambee removeu suas contas gratuitas (limitadas) e n\u00e3o oferece mais uma maneira de usu\u00e1rios regulares se inscreverem em um plano pago. \n\n Remova a entrada de integra\u00e7\u00e3o Ambee de sua inst\u00e2ncia para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o Ambee est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json index 2e1de25fde2..ccebea49c6f 100644 --- a/homeassistant/components/ambee/translations/zh-Hant.json +++ b/homeassistant/components/ambee/translations/zh-Hant.json @@ -24,5 +24,11 @@ "description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" } } + }, + "issues": { + "pending_removal": { + "description": "Ambee \u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc Ambee \u79fb\u9664\u4e86\u5176\u514d\u8cbb\uff08\u6709\u9650\uff09\u5e33\u865f\u3001\u4e26\u4e14\u4e0d\u518d\u63d0\u4f9b\u4e00\u822c\u4f7f\u7528\u8005\u8a3b\u518a\u4ed8\u8cbb\u670d\u52d9\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Ambee \u6574\u5408\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/de.json b/homeassistant/components/anthemav/translations/de.json index 622384629fe..d751349b005 100644 --- a/homeassistant/components/anthemav/translations/de.json +++ b/homeassistant/components/anthemav/translations/de.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Anthem A/V-Receivern mit YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Anthem A/V Receivers YAML Konfiguration aus deiner configuration.yaml Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die YAML-Konfiguration von Anthem A/V Receivers wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json index 12b0df56f0f..b8bec832581 100644 --- a/homeassistant/components/anthemav/translations/it.json +++ b/homeassistant/components/anthemav/translations/it.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Anthem A/V Receivers tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Anthem A/V Receivers verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json index 309aca9b8ef..5a6038bb480 100644 --- a/homeassistant/components/anthemav/translations/pt-BR.json +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o de receptores A/V Anthem usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML dos receptores A/V do Anthem do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML dos receptores A/V do Anthem est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json index d1b286afd81..0751331f82f 100644 --- a/homeassistant/components/anthemav/translations/zh-Hant.json +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index c29eb8d1a2c..6e24178996f 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -36,7 +36,7 @@ "issues": { "deprecated_yaml": { "description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Google Calendar \u00e8 stata rimossa" + "title": "La configurazione YAML di Google Calendar verr\u00e0 rimossa" }, "removed_track_new_yaml": { "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 43c208d69e8..0d2031c368f 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -36,7 +36,7 @@ "issues": { "deprecated_yaml": { "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" }, "removed_track_new_yaml": { "description": "\u65bc configuration.yaml \u5167\u6240\u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u529f\u80fd\uff0c\u7531\u65bc\u4e0d\u518d\u652f\u6301\u3001\u5df2\u7d93\u906d\u5230\u95dc\u9589\u3002\u4e4b\u5f8c\u5fc5\u9808\u624b\u52d5\u900f\u904e\u4ecb\u9762\u5167\u7684\u6574\u5408\u529f\u80fd\u3001\u4ee5\u95dc\u9589\u4efb\u4f55\u65b0\u767c\u73fe\u7684\u5be6\u9ad4\u3002\u8acb\u7531 configuration.yaml \u4e2d\u79fb\u9664R track_new \u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", diff --git a/homeassistant/components/homeassistant_alerts/translations/de.json b/homeassistant/components/homeassistant_alerts/translations/de.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/en.json b/homeassistant/components/homeassistant_alerts/translations/en.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/it.json b/homeassistant/components/homeassistant_alerts/translations/it.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pl.json b/homeassistant/components/homeassistant_alerts/translations/pl.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pt-BR.json b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/de.json b/homeassistant/components/lacrosse_view/translations/de.json new file mode 100644 index 00000000000..d9aa1210fa0 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_locations": "Keine Standorte gefunden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json new file mode 100644 index 00000000000..a2a7fd23272 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "no_locations": "No locations found", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/it.json b/homeassistant/components/lacrosse_view/translations/it.json new file mode 100644 index 00000000000..9ce6c75dcbc --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "no_locations": "Nessuna localit\u00e0 trovata", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/pt-BR.json b/homeassistant/components/lacrosse_view/translations/pt-BR.json new file mode 100644 index 00000000000..29b458e5599 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_locations": "Nenhum local encontrado", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/zh-Hant.json b/homeassistant/components/lacrosse_view/translations/zh-Hant.json new file mode 100644 index 00000000000..78235452297 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_locations": "\u627e\u4e0d\u5230\u5ea7\u6a19", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json index b067b299145..1ef7e65fbf4 100644 --- a/homeassistant/components/lyric/translations/de.json +++ b/homeassistant/components/lyric/translations/de.json @@ -17,5 +17,11 @@ "title": "Integration erneut authentifizieren" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Honeywell Lyric mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Honeywell Lyric YAML-Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pt-BR.json b/homeassistant/components/lyric/translations/pt-BR.json index 907a396d5e2..d70832bfbf6 100644 --- a/homeassistant/components/lyric/translations/pt-BR.json +++ b/homeassistant/components/lyric/translations/pt-BR.json @@ -17,5 +17,11 @@ "title": "Reautenticar Integra\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Honeywell Lyric usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Honeywell Lyric YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json index 850507ec0b3..bb7fbc3aed6 100644 --- a/homeassistant/components/lyric/translations/zh-Hant.json +++ b/homeassistant/components/lyric/translations/zh-Hant.json @@ -17,5 +17,11 @@ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Honeywell Lyric \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Honeywell Lyric YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/de.json b/homeassistant/components/mitemp_bt/translations/de.json index 7d4887ab638..3c9e6960aeb 100644 --- a/homeassistant/components/mitemp_bt/translations/de.json +++ b/homeassistant/components/mitemp_bt/translations/de.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t manuell mit der neuen Integration hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t mit der neuen Integration manuell hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", "title": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors wurde ersetzt" } } diff --git a/homeassistant/components/mitemp_bt/translations/it.json b/homeassistant/components/mitemp_bt/translations/it.json new file mode 100644 index 00000000000..cc383e4184c --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor \u00e8 stata sostituita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pt-BR.json b/homeassistant/components/mitemp_bt/translations/pt-BR.json index 991a749a729..634f5dd71fd 100644 --- a/homeassistant/components/mitemp_bt/translations/pt-BR.json +++ b/homeassistant/components/mitemp_bt/translations/pt-BR.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade do Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE foi substitu\u00edda" } } diff --git a/homeassistant/components/openalpr_local/translations/de.json b/homeassistant/components/openalpr_local/translations/de.json new file mode 100644 index 00000000000..d517fe0b37f --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die lokale OpenALPR-Integration wird derzeit aus dem Home Assistant entfernt und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die lokale OpenALPR-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/it.json b/homeassistant/components/openalpr_local/translations/it.json new file mode 100644 index 00000000000..26ce80ee584 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione OpenALPR Local \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "L'integrazione OpenALPR Local verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pl.json b/homeassistant/components/openalpr_local/translations/pl.json new file mode 100644 index 00000000000..ac367d20809 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja OpenALPR Local oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja OpenALPR Local zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pt-BR.json b/homeassistant/components/openalpr_local/translations/pt-BR.json new file mode 100644 index 00000000000..96b2c244b5c --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/zh-Hant.json b/homeassistant/components/openalpr_local/translations/zh-Hant.json new file mode 100644 index 00000000000..8ec55e5a004 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index 9e35beb648a..fef1c64746b 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -21,7 +21,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML \u00e8 stata rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema." + "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Radio Thermostat verr\u00e0 rimossa" } }, "options": { diff --git a/homeassistant/components/senz/translations/de.json b/homeassistant/components/senz/translations/de.json index ffbc7bb458f..fbae91321be 100644 --- a/homeassistant/components/senz/translations/de.json +++ b/homeassistant/components/senz/translations/de.json @@ -16,5 +16,11 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von nVent RAYCHEM SENZ mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die nVent RAYCHEM SENZ YAML Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pt-BR.json b/homeassistant/components/senz/translations/pt-BR.json index 7e3ff2f64a9..02c31d97816 100644 --- a/homeassistant/components/senz/translations/pt-BR.json +++ b/homeassistant/components/senz/translations/pt-BR.json @@ -16,5 +16,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do nVent RAYCHEM SENZ usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o nVent RAYCHEM SENZ YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/zh-Hant.json b/homeassistant/components/senz/translations/zh-Hant.json index 3bf08cf34c7..7094ee18a02 100644 --- a/homeassistant/components/senz/translations/zh-Hant.json +++ b/homeassistant/components/senz/translations/zh-Hant.json @@ -16,5 +16,11 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a nVent RAYCHEM SENZ \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "nVent RAYCHEM SENZ YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/de.json b/homeassistant/components/soundtouch/translations/de.json index 8d28b988834..379516e31be 100644 --- a/homeassistant/components/soundtouch/translations/de.json +++ b/homeassistant/components/soundtouch/translations/de.json @@ -17,5 +17,11 @@ "title": "Best\u00e4tige das Hinzuf\u00fcgen des Bose SoundTouch-Ger\u00e4ts" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Bose SoundTouch mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Bose SoundTouch YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Bose SoundTouch YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/it.json b/homeassistant/components/soundtouch/translations/it.json index a14492bf5c3..f9c6d512b2a 100644 --- a/homeassistant/components/soundtouch/translations/it.json +++ b/homeassistant/components/soundtouch/translations/it.json @@ -17,5 +17,11 @@ "title": "Conferma l'aggiunta del dispositivo Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Bose SoundTouch tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Bose SoundTouch dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Bose SoundTouch verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt-BR.json b/homeassistant/components/soundtouch/translations/pt-BR.json index e707219f342..7446cbc5a06 100644 --- a/homeassistant/components/soundtouch/translations/pt-BR.json +++ b/homeassistant/components/soundtouch/translations/pt-BR.json @@ -17,5 +17,11 @@ "title": "Confirme a adi\u00e7\u00e3o do dispositivo Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Bose SoundTouch usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Bose SoundTouch do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Bose SoundTouch est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json index 08231b8571d..f3d8e8e8560 100644 --- a/homeassistant/components/soundtouch/translations/zh-Hant.json +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -17,5 +17,11 @@ "title": "\u78ba\u8a8d\u65b0\u589e Bose SoundTouch \u88dd\u7f6e" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Bose SoundTouch YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index ce89e224a8c..52773f3e411 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -21,8 +21,8 @@ }, "issues": { "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Spotify YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Spotify YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } }, "system_health": { diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json index e3c725532e9..8b0c932735b 100644 --- a/homeassistant/components/steam_online/translations/zh-Hant.json +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -26,8 +26,8 @@ }, "issues": { "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Steam YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Steam YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } }, "options": { diff --git a/homeassistant/components/uscis/translations/zh-Hant.json b/homeassistant/components/uscis/translations/zh-Hant.json index 4a5882dbd95..ccc72d3d1dd 100644 --- a/homeassistant/components/uscis/translations/zh-Hant.json +++ b/homeassistant/components/uscis/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "issues": { "pending_removal": { - "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u6b63\u8a08\u5283\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u8acb\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u6574\u5408\u6b63\u5728\u79fb\u9664\u4e2d\u3001\u7531\u65bc\u4f7f\u7528\u4e86\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u5c07\u4e0d\u88ab\u5141\u8a31\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "USCIS \u6574\u5408\u6b63\u6e96\u5099\u79fb\u9664" + "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc\u4f7f\u7528\u4e86\u4e0d\u88ab\u5141\u8a31\u7684\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "USCIS \u6574\u5408\u5373\u5c07\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/it.json b/homeassistant/components/xbox/translations/it.json index e60c37c9e5f..6cf5bf15bb9 100644 --- a/homeassistant/components/xbox/translations/it.json +++ b/homeassistant/components/xbox/translations/it.json @@ -13,5 +13,11 @@ "title": "Scegli il metodo di autenticazione" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Xbox in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Xbox verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/pt-BR.json b/homeassistant/components/xbox/translations/pt-BR.json index 7f788c1ebb8..d1bb02e84dd 100644 --- a/homeassistant/components/xbox/translations/pt-BR.json +++ b/homeassistant/components/xbox/translations/pt-BR.json @@ -13,5 +13,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Xbox em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Xbox est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 9b8acdf5b87..b4ad58636a7 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -46,11 +46,13 @@ "title": "Optionen f\u00fcr die Alarmsteuerung" }, "zha_options": { + "always_prefer_xy_color_mode": "Immer den XY-Farbmodus bevorzugen", "consider_unavailable_battery": "Batteriebetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", + "light_transitioning_flag": "Erweiterten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 0b8e90f5ac0..99c519782db 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -46,11 +46,13 @@ "title": "Opcje panelu alarmowego" }, "zha_options": { + "always_prefer_xy_color_mode": "Zawsze preferuj tryb kolor\u00f3w XY", "consider_unavailable_battery": "Uznaj urz\u0105dzenia zasilane bateryjnie za niedost\u0119pne po (sekundach)", "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z gniazdka za niedost\u0119pne po (sekundach)", "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", "enhanced_light_transition": "W\u0142\u0105cz ulepszone przej\u015bcie koloru \u015bwiat\u0142a/temperatury ze stanu wy\u0142\u0105czenia", + "light_transitioning_flag": "W\u0142\u0105cz suwak zwi\u0119kszonej jasno\u015bci podczas przej\u015bcia \u015bwiat\u0142a", "title": "Opcje og\u00f3lne" } }, diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index ba54b4aba87..69b8ced6970 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -46,11 +46,13 @@ "title": "Op\u00e7\u00f5es do painel de controle de alarme" }, "zha_options": { + "always_prefer_xy_color_mode": "Sempre prefira o modo de cor XY", "consider_unavailable_battery": "Considerar dispositivos alimentados por bateria indispon\u00edveis ap\u00f3s (segundos)", "consider_unavailable_mains": "Considerar os dispositivos alimentados pela rede indispon\u00edveis ap\u00f3s (segundos)", "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz padr\u00e3o (segundos)", "enable_identify_on_join": "Ativar o efeito de identifica\u00e7\u00e3o quando os dispositivos ingressarem na rede", "enhanced_light_transition": "Ative a transi\u00e7\u00e3o de cor/temperatura da luz aprimorada de um estado desligado", + "light_transitioning_flag": "Ative o controle deslizante de brilho aprimorado durante a transi\u00e7\u00e3o de luz", "title": "Op\u00e7\u00f5es globais" } }, diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 9505da31e80..546a2f77c31 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -46,11 +46,13 @@ "title": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u9078\u9805" }, "zha_options": { + "always_prefer_xy_color_mode": "\u504f\u597d XY \u8272\u5f69\u6a21\u5f0f", "consider_unavailable_battery": "\u5c07\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "consider_unavailable_mains": "\u5c07\u4e3b\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", "enhanced_light_transition": "\u958b\u555f\u7531\u95dc\u9589\u72c0\u614b\u589e\u5f37\u5149\u8272/\u8272\u6eab\u8f49\u63db", + "light_transitioning_flag": "\u958b\u555f\u71c8\u5149\u8f49\u63db\u589e\u5f37\u4eae\u5ea6\u8abf\u6574\u5217", "title": "Global \u9078\u9805" } }, From 3b8650d05353ad2c43d8175469f162ca6b0015b7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 28 Jul 2022 11:41:03 +0200 Subject: [PATCH 740/820] Make Axis utilise forward_entry_setups (#75178) --- homeassistant/components/axis/__init__.py | 26 ++++-- homeassistant/components/axis/config_flow.py | 11 +-- homeassistant/components/axis/device.py | 86 ++++++++------------ tests/components/axis/test_config_flow.py | 4 +- tests/components/axis/test_device.py | 17 ++-- 5 files changed, 62 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 5e211c00028..4af066f4e89 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -4,28 +4,36 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.entity_registry import async_migrate_entries -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice +from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS +from .device import AxisNetworkDevice, get_axis_device +from .errors import AuthenticationRequired, CannotConnect _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Set up the Axis component.""" + """Set up the Axis integration.""" hass.data.setdefault(AXIS_DOMAIN, {}) - device = AxisNetworkDevice(hass, config_entry) - - if not await device.async_setup(): - return False - - hass.data[AXIS_DOMAIN][config_entry.unique_id] = device + try: + api = await get_axis_device(hass, config_entry.data) + except CannotConnect as err: + raise ConfigEntryNotReady from err + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] = AxisNetworkDevice( + hass, config_entry, api + ) await device.async_update_device_registry() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + device.async_setup_events() + config_entry.add_update_listener(device.async_new_address_callback) config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) ) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index f94c27dc2ac..1ce2f08c045 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping from ipaddress import ip_address +from types import MappingProxyType from typing import Any from urllib.parse import urlsplit @@ -32,7 +33,7 @@ from .const import ( DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) -from .device import AxisNetworkDevice, get_device +from .device import AxisNetworkDevice, get_axis_device from .errors import AuthenticationRequired, CannotConnect AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} @@ -66,13 +67,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): if user_input is not None: try: - device = await get_device( - self.hass, - host=user_input[CONF_HOST], - port=user_input[CONF_PORT], - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - ) + device = await get_axis_device(self.hass, MappingProxyType(user_input)) serial = device.vapix.serial_number await self.async_set_unique_id(format_mac(serial)) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index d0d5e230d2f..683991d0f65 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,6 +1,8 @@ """Axis network device abstraction.""" import asyncio +from types import MappingProxyType +from typing import Any import async_timeout import axis @@ -24,7 +26,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -50,15 +51,15 @@ from .errors import AuthenticationRequired, CannotConnect class AxisNetworkDevice: """Manages a Axis device.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry, api): """Initialize the device.""" self.hass = hass self.config_entry = config_entry - self.available = True + self.api = api - self.api = None - self.fw_version = None - self.product_type = None + self.available = True + self.fw_version = api.vapix.firmware_version + self.product_type = api.vapix.product_type @property def host(self): @@ -184,7 +185,7 @@ class AxisNetworkDevice: sw_version=self.fw_version, ) - async def use_mqtt(self, hass: HomeAssistant, component: str) -> None: + async def async_use_mqtt(self, hass: HomeAssistant, component: str) -> None: """Set up to use MQTT.""" try: status = await self.api.vapix.mqtt.get_client_status() @@ -209,50 +210,18 @@ class AxisNetworkDevice: # Setup and teardown methods - async def async_setup(self): - """Set up the device.""" - try: - self.api = await get_device( - self.hass, - host=self.host, - port=self.port, - username=self.username, - password=self.password, + def async_setup_events(self): + """Set up the device events.""" + + if self.option_events: + self.api.stream.connection_status_callback.append( + self.async_connection_status_callback ) + self.api.enable_events(event_callback=self.async_event_callback) + self.api.stream.start() - except CannotConnect as err: - raise ConfigEntryNotReady from err - - except AuthenticationRequired as err: - raise ConfigEntryAuthFailed from err - - self.fw_version = self.api.vapix.firmware_version - self.product_type = self.api.vapix.product_type - - async def start_platforms(): - await asyncio.gather( - *( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - for platform in PLATFORMS - ) - ) - if self.option_events: - self.api.stream.connection_status_callback.append( - self.async_connection_status_callback - ) - self.api.enable_events(event_callback=self.async_event_callback) - self.api.stream.start() - - if self.api.vapix.mqtt: - async_when_setup(self.hass, MQTT_DOMAIN, self.use_mqtt) - - self.hass.async_create_task(start_platforms()) - - self.config_entry.add_update_listener(self.async_new_address_callback) - - return True + if self.api.vapix.mqtt: + async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) @callback def disconnect_from_stream(self): @@ -274,14 +243,21 @@ class AxisNetworkDevice: ) -async def get_device( - hass: HomeAssistant, host: str, port: int, username: str, password: str +async def get_axis_device( + hass: HomeAssistant, + config: MappingProxyType[str, Any], ) -> axis.AxisDevice: """Create a Axis device.""" session = get_async_client(hass, verify_ssl=False) device = axis.AxisDevice( - Configuration(session, host, port=port, username=username, password=password) + Configuration( + session, + config[CONF_HOST], + port=config[CONF_PORT], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + ) ) try: @@ -291,11 +267,13 @@ async def get_device( return device except axis.Unauthorized as err: - LOGGER.warning("Connected to device at %s but not registered", host) + LOGGER.warning( + "Connected to device at %s but not registered", config[CONF_HOST] + ) raise AuthenticationRequired from err except (asyncio.TimeoutError, axis.RequestError) as err: - LOGGER.error("Error connecting to the Axis device at %s", host) + LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) raise CannotConnect from err except axis.AxisException as err: diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 1459ae215d9..2daf350ac93 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -123,7 +123,7 @@ async def test_flow_fails_faulty_credentials(hass): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.AuthenticationRequired, ): result = await hass.config_entries.flow.async_configure( @@ -149,7 +149,7 @@ async def test_flow_fails_cannot_connect(hass): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.CannotConnect, ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 4717e2915c1..ba6df6e2e2d 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -441,7 +441,7 @@ async def test_device_reset(hass): async def test_device_not_accessible(hass): """Failed setup schedules a retry of setup.""" - with patch.object(axis.device, "get_device", side_effect=axis.errors.CannotConnect): + with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -449,7 +449,7 @@ async def test_device_not_accessible(hass): async def test_device_trigger_reauth_flow(hass): """Failed authentication trigger a reauthentication flow.""" with patch.object( - axis.device, "get_device", side_effect=axis.errors.AuthenticationRequired + axis, "get_axis_device", side_effect=axis.errors.AuthenticationRequired ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await setup_axis_integration(hass) mock_flow_init.assert_called_once() @@ -458,7 +458,7 @@ async def test_device_trigger_reauth_flow(hass): async def test_device_unknown_error(hass): """Unknown errors are handled.""" - with patch.object(axis.device, "get_device", side_effect=Exception): + with patch.object(axis, "get_axis_device", side_effect=Exception): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -468,7 +468,7 @@ async def test_new_event_sends_signal(hass): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send: axis_device.async_event_callback(action=OPERATION_INITIALIZED, event_id="event") @@ -484,8 +484,7 @@ async def test_shutdown(): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) - axis_device.api = Mock() + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) await axis_device.shutdown(None) @@ -497,7 +496,7 @@ async def test_get_device_fails(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_device_unavailable(hass): @@ -505,7 +504,7 @@ async def test_get_device_device_unavailable(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_unknown_error(hass): @@ -513,4 +512,4 @@ async def test_get_device_unknown_error(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) From 70731c0bc7042da51db5bc0a122a8a0f1f5e86eb Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 27 Jul 2022 16:06:33 -0400 Subject: [PATCH 741/820] Add Insteon lock and load controller devices (#75632) --- homeassistant/components/insteon/const.py | 1 + homeassistant/components/insteon/ipdb.py | 5 + homeassistant/components/insteon/lock.py | 49 ++++++++ .../components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/insteon/mock_devices.py | 21 +++- tests/components/insteon/test_lock.py | 109 ++++++++++++++++++ 8 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/insteon/lock.py create mode 100644 tests/components/insteon/test_lock.py diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index fb7b2387d73..5337ccd36c3 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -44,6 +44,7 @@ INSTEON_PLATFORMS = [ Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SWITCH, ] diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 6866e052368..7f4ff92380f 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -1,5 +1,6 @@ """Utility methods for the Insteon platform.""" from pyinsteon.device_types import ( + AccessControl_Morningstar, ClimateControl_Thermostat, ClimateControl_WirelessThermostat, DimmableLightingControl, @@ -12,6 +13,7 @@ from pyinsteon.device_types import ( DimmableLightingControl_OutletLinc, DimmableLightingControl_SwitchLinc, DimmableLightingControl_ToggleLinc, + EnergyManagement_LoadController, GeneralController_ControlLinc, GeneralController_MiniRemote_4, GeneralController_MiniRemote_8, @@ -44,11 +46,13 @@ from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.switch import DOMAIN as SWITCH from .const import ON_OFF_EVENTS DEVICE_PLATFORM = { + AccessControl_Morningstar: {LOCK: [1]}, DimmableLightingControl: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_DinRail: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_FanLinc: {LIGHT: [1], FAN: [2], ON_OFF_EVENTS: [1, 2]}, @@ -67,6 +71,7 @@ DEVICE_PLATFORM = { DimmableLightingControl_OutletLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_SwitchLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_ToggleLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + EnergyManagement_LoadController: {SWITCH: [1], BINARY_SENSOR: [2]}, GeneralController_ControlLinc: {ON_OFF_EVENTS: [1]}, GeneralController_MiniRemote_4: {ON_OFF_EVENTS: range(1, 5)}, GeneralController_MiniRemote_8: {ON_OFF_EVENTS: range(1, 9)}, diff --git a/homeassistant/components/insteon/lock.py b/homeassistant/components/insteon/lock.py new file mode 100644 index 00000000000..17a7cf20111 --- /dev/null +++ b/homeassistant/components/insteon/lock.py @@ -0,0 +1,49 @@ +"""Support for INSTEON locks.""" + +from typing import Any + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import SIGNAL_ADD_ENTITIES +from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Insteon locks from a config entry.""" + + @callback + def async_add_insteon_lock_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, LOCK_DOMAIN, InsteonLockEntity, async_add_entities, discovery_info + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{LOCK_DOMAIN}" + async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities) + async_add_insteon_lock_entities() + + +class InsteonLockEntity(InsteonEntity, LockEntity): + """A Class for an Insteon lock entity.""" + + @property + def is_locked(self) -> bool: + """Return the boolean response if the node is on.""" + return bool(self._insteon_device_group.value) + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the device.""" + await self._insteon_device.async_lock() + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the device.""" + await self._insteon_device.async_unlock() diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c48d502c16e..577383e8976 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.3", + "pyinsteon==1.2.0", "insteon-frontend-home-assistant==0.2.0" ], "codeowners": ["@teharris1"], diff --git a/requirements_all.txt b/requirements_all.txt index ea3f2dd3e6c..57ddf87a256 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1569,7 +1569,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f19f813747..1f650a56a30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1076,7 +1076,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index ef64b1e0969..417769d6696 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from pyinsteon.address import Address from pyinsteon.constants import ALDBStatus, ResponseStatus from pyinsteon.device_types import ( + AccessControl_Morningstar, DimmableLightingControl_KeypadLinc_8, GeneralController_RemoteLinc, Hub, @@ -59,12 +60,13 @@ class MockDevices: async def async_load(self, *args, **kwargs): """Load the mock devices.""" - if self._connected: + if self._connected and not self._devices: addr0 = Address("AA.AA.AA") addr1 = Address("11.11.11") addr2 = Address("22.22.22") addr3 = Address("33.33.33") addr4 = Address("44.44.44") + addr5 = Address("55.55.55") self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0") self._devices[addr1] = MockSwitchLinc( addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1" @@ -78,9 +80,12 @@ class MockDevices: self._devices[addr4] = SensorsActuators_IOLink( addr4, 0x07, 0x00, 0x00, "Device 44.44.44", "4" ) + self._devices[addr5] = AccessControl_Morningstar( + addr5, 0x0F, 0x0A, 0x00, "Device 55.55.55", "5" + ) for device in [ - self._devices[addr] for addr in [addr1, addr2, addr3, addr4] + self._devices[addr] for addr in [addr1, addr2, addr3, addr4, addr5] ]: device.async_read_config = AsyncMock() device.aldb.async_write = AsyncMock() @@ -99,7 +104,9 @@ class MockDevices: return_value=ResponseStatus.SUCCESS ) - for device in [self._devices[addr] for addr in [addr2, addr3, addr4]]: + for device in [ + self._devices[addr] for addr in [addr2, addr3, addr4, addr5] + ]: device.async_status = AsyncMock() self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) self._devices[addr0].aldb.async_load = AsyncMock() @@ -117,6 +124,12 @@ class MockDevices: return_value=ResponseStatus.FAILURE ) + self._devices[addr5].async_lock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) + self._devices[addr5].async_unlock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) self.modem = self._devices[addr0] self.modem.async_read_config = AsyncMock() @@ -155,6 +168,6 @@ class MockDevices: yield address await asyncio.sleep(0.01) - def subscribe(self, listener): + def subscribe(self, listener, force_strong_ref=False): """Mock the subscribe function.""" subscribe_topic(listener, DEVICE_LIST_CHANGED) diff --git a/tests/components/insteon/test_lock.py b/tests/components/insteon/test_lock.py new file mode 100644 index 00000000000..6f847543a9f --- /dev/null +++ b/tests/components/insteon/test_lock.py @@ -0,0 +1,109 @@ +"""Tests for the Insteon lock.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components import insteon +from homeassistant.components.insteon import ( + DOMAIN, + insteon_entity, + utils as insteon_utils, +) +from homeassistant.components.lock import ( # SERVICE_LOCK,; SERVICE_UNLOCK, + DOMAIN as LOCK_DOMAIN, +) +from homeassistant.const import ( # ATTR_ENTITY_ID,; + EVENT_HOMEASSISTANT_STOP, + STATE_LOCKED, + STATE_UNLOCKED, + Platform, +) +from homeassistant.helpers import entity_registry as er + +from .const import MOCK_USER_INPUT_PLM +from .mock_devices import MockDevices + +from tests.common import MockConfigEntry + +devices = MockDevices() + + +@pytest.fixture(autouse=True) +def lock_platform_only(): + """Only setup the lock and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.insteon.INSTEON_PLATFORMS", + (Platform.LOCK,), + ): + yield + + +@pytest.fixture(autouse=True) +def patch_setup_and_devices(): + """Patch the Insteon setup process and devices.""" + with patch.object(insteon, "async_connect", new=mock_connection), patch.object( + insteon, "async_close" + ), patch.object(insteon, "devices", devices), patch.object( + insteon_utils, "devices", devices + ), patch.object( + insteon_entity, "devices", devices + ): + yield + + +async def mock_connection(*args, **kwargs): + """Return a successful connection.""" + return True + + +async def test_lock_lock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + assert state.state is STATE_UNLOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "lock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_lock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_lock_unlock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + devices["55.55.55"].groups[1].set_value(255) + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + + assert state.state is STATE_LOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "unlock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_unlock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() From 7811518d7c6c538c32242e4f8e575157b80da03a Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 27 Jul 2022 16:39:39 -0500 Subject: [PATCH 742/820] Add Leviton as a supported brand of ZwaveJS (#75729) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/manifest.json | 5 ++++- homeassistant/generated/supported_brands.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 76f5d94b589..8b6ecefc5f5 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -30,5 +30,8 @@ } ], "zeroconf": ["_zwave-js-server._tcp.local."], - "loggers": ["zwave_js_server"] + "loggers": ["zwave_js_server"], + "supported_brands": { + "leviton_z_wave": "Leviton Z-Wave" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 589e0462cf7..4e151f5578d 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -11,5 +11,6 @@ HAS_SUPPORTED_BRANDS = ( "motion_blinds", "overkiz", "renault", - "wemo" + "wemo", + "zwave_js" ) From 15e6fcca41f42ba666881f8170207613a2104884 Mon Sep 17 00:00:00 2001 From: Brandon West Date: Thu, 28 Jul 2022 12:27:48 -0400 Subject: [PATCH 743/820] Bump russound_rio to 0.1.8 (#75837) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 4b9b7a2c8d0..e844f478322 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -2,7 +2,7 @@ "domain": "russound_rio", "name": "Russound RIO", "documentation": "https://www.home-assistant.io/integrations/russound_rio", - "requirements": ["russound_rio==0.1.7"], + "requirements": ["russound_rio==0.1.8"], "codeowners": [], "iot_class": "local_push", "loggers": ["russound_rio"] diff --git a/requirements_all.txt b/requirements_all.txt index 57ddf87a256..95da109691c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2123,7 +2123,7 @@ rtsp-to-webrtc==0.5.1 russound==0.1.9 # homeassistant.components.russound_rio -russound_rio==0.1.7 +russound_rio==0.1.8 # homeassistant.components.yamaha rxv==0.7.0 From 96587c1227efae30865acc9f074dce62cab229a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jul 2022 12:59:43 -0700 Subject: [PATCH 744/820] Remove learn more URL from Home Assistant alerts (#75838) --- .../homeassistant_alerts/__init__.py | 5 +-- .../fixtures/alerts_no_url.json | 34 ------------------- .../homeassistant_alerts/test_init.py | 15 +++----- 3 files changed, 5 insertions(+), 49 deletions(-) delete mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_url.json diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 1aedd6c5419..12ba4dce8ba 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -75,7 +75,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, issue_id, is_fixable=False, - learn_more_url=alert.alert_url, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ @@ -112,7 +111,6 @@ class IntegrationAlert: integration: str filename: str date_updated: str | None - alert_url: str | None @property def issue_id(self) -> str: @@ -147,7 +145,7 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) result = {} for alert in alerts: - if "alert_url" not in alert or "integrations" not in alert: + if "integrations" not in alert: continue if "homeassistant" in alert: @@ -177,7 +175,6 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) integration=integration["package"], filename=alert["filename"], date_updated=alert.get("date_updated"), - alert_url=alert["alert_url"], ) result[integration_alert.issue_id] = integration_alert diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json deleted file mode 100644 index 89f277cf69b..00000000000 --- a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "title": "Dark Sky API closed for new users", - "created": "2020-03-31T14:40:00.000Z", - "integrations": [ - { - "package": "darksky" - } - ], - "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", - "homeassistant": { - "package": "homeassistant", - "affected_from_version": "0.30" - }, - "filename": "dark_sky.markdown", - "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" - }, - { - "title": "Hikvision Security Vulnerability", - "created": "2021-09-20T22:08:00.000Z", - "integrations": [ - { - "package": "hikvision" - }, - { - "package": "hikvisioncam" - } - ], - "filename": "hikvision.markdown", - "homeassistant": { - "package": "homeassistant" - } - } -] diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index c0b6f471033..cb39fb73108 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -133,7 +133,7 @@ async def test_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -149,13 +149,6 @@ async def test_alerts( @pytest.mark.parametrize( "ha_version, fixture, expected_alerts", ( - ( - "2022.7.0", - "alerts_no_url.json", - [ - ("dark_sky.markdown", "darksky"), - ], - ), ( "2022.7.0", "alerts_no_integrations.json", @@ -220,7 +213,7 @@ async def test_bad_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -381,7 +374,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -420,7 +413,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { From 937fd490f2d8326e4175e98ce78d26ef2a4e8243 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jul 2022 13:53:51 -0700 Subject: [PATCH 745/820] Add issue_domain to repairs (#75839) --- .../components/homeassistant_alerts/__init__.py | 1 + homeassistant/components/repairs/issue_handler.py | 2 ++ homeassistant/components/repairs/issue_registry.py | 6 ++++++ tests/components/demo/test_init.py | 5 +++++ tests/components/homeassistant_alerts/test_init.py | 4 ++++ tests/components/repairs/test_init.py | 13 +++++++++++++ tests/components/repairs/test_websocket_api.py | 7 +++++++ 7 files changed, 38 insertions(+) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 12ba4dce8ba..d405b9e257d 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -75,6 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, issue_id, is_fixable=False, + issue_domain=alert.integration, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 8eff4ac64fe..c139026ec48 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -86,6 +86,7 @@ def async_create_issue( domain: str, issue_id: str, *, + issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, learn_more_url: str | None = None, @@ -106,6 +107,7 @@ def async_create_issue( issue_registry.async_get_or_create( domain, issue_id, + issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, learn_more_url=learn_more_url, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index 5c459309cc0..bd201f1007c 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -30,6 +30,8 @@ class IssueEntry: domain: str is_fixable: bool | None issue_id: str + # Used if an integration creates issues for other integrations (ie alerts) + issue_domain: str | None learn_more_url: str | None severity: IssueSeverity | None translation_key: str | None @@ -58,6 +60,7 @@ class IssueRegistry: domain: str, issue_id: str, *, + issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, learn_more_url: str | None = None, @@ -75,6 +78,7 @@ class IssueRegistry: dismissed_version=None, domain=domain, is_fixable=is_fixable, + issue_domain=issue_domain, issue_id=issue_id, learn_more_url=learn_more_url, severity=severity, @@ -93,6 +97,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -155,6 +160,7 @@ class IssueRegistry: domain=issue["domain"], is_fixable=None, issue_id=issue["issue_id"], + issue_domain=None, learn_more_url=None, severity=None, translation_key=None, diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 85ff2a16405..5b322cb776f 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -95,6 +95,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "transmogrifier_deprecated", + "issue_domain": None, "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", "severity": "warning", "translation_key": "transmogrifier_deprecated", @@ -108,6 +109,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": True, "issue_id": "out_of_blinker_fluid", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", "severity": "critical", "translation_key": "out_of_blinker_fluid", @@ -121,6 +123,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "unfixable_problem", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "severity": "warning", "translation_key": "unfixable_problem", @@ -180,6 +183,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "transmogrifier_deprecated", + "issue_domain": None, "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", "severity": "warning", "translation_key": "transmogrifier_deprecated", @@ -193,6 +197,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "unfixable_problem", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "severity": "warning", "translation_key": "unfixable_problem", diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index cb39fb73108..a0fb2e8557d 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -133,6 +133,7 @@ async def test_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -213,6 +214,7 @@ async def test_bad_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -374,6 +376,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -413,6 +416,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 2f82a084968..d70f6c6e11d 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -85,6 +85,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -97,6 +98,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], is_fixable=issues[0]["is_fixable"], + issue_domain="my_issue_domain", learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -113,6 +115,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=None, ignored=False, learn_more_url="blablabla", + issue_domain="my_issue_domain", ) @@ -206,6 +209,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -226,6 +230,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -245,6 +250,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -264,6 +270,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -292,6 +299,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=ha_version, ignored=True, learn_more_url="blablabla", + issue_domain=None, ) # Unignore the same issue @@ -309,6 +317,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=None, ignored=False, learn_more_url="blablabla", + issue_domain=None, ) for issue in issues ] @@ -359,6 +368,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -378,6 +388,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -428,6 +439,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T08:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -501,6 +513,7 @@ async def test_sync_methods( "ignored": False, "is_fixable": True, "issue_id": "sync_issue", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 73d1898fcb7..d778b043832 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -61,6 +61,7 @@ async def create_issues(hass, ws_client): created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -154,6 +155,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: created=ANY, dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -183,6 +185,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -226,6 +229,7 @@ async def test_fix_non_existing_issue( created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -383,6 +387,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "dismissed_version": None, "domain": "test", "issue_id": "issue_3_inactive", + "issue_domain": None, }, ] }, @@ -404,6 +409,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "domain": "test", "is_fixable": True, "issue_id": "issue_1", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -414,6 +420,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "domain": "test", "is_fixable": False, "issue_id": "issue_2", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", From add9ff57363a3fef6b11cbeaa2aad25c164ae29b Mon Sep 17 00:00:00 2001 From: Rolf Berkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Wed, 27 Jul 2022 23:50:41 +0200 Subject: [PATCH 746/820] Fix fetching MeteoAlarm XML data (#75840) --- homeassistant/components/meteoalarm/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 35333f6ea01..9a3da54d34f 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -2,7 +2,7 @@ "domain": "meteoalarm", "name": "MeteoAlarm", "documentation": "https://www.home-assistant.io/integrations/meteoalarm", - "requirements": ["meteoalertapi==0.2.0"], + "requirements": ["meteoalertapi==0.3.0"], "codeowners": ["@rolfberkenbosch"], "iot_class": "cloud_polling", "loggers": ["meteoalertapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 95da109691c..9c48e9984eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1025,7 +1025,7 @@ meater-python==0.0.8 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.2.0 +meteoalertapi==0.3.0 # homeassistant.components.meteo_france meteofrance-api==1.0.2 From e7ff97bac0240ee87989de01428e905137a71f40 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 22:58:58 +0200 Subject: [PATCH 747/820] Fix temperature unit in evohome (#75842) --- homeassistant/components/evohome/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 841619af6f1..c1a630d0d05 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -126,6 +126,8 @@ async def async_setup_platform( class EvoClimateEntity(EvoDevice, ClimateEntity): """Base for an evohome Climate device.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Climate device.""" super().__init__(evo_broker, evo_device) @@ -316,7 +318,6 @@ class EvoController(EvoClimateEntity): _attr_icon = "mdi:thermostat" _attr_precision = PRECISION_TENTHS - _attr_temperature_unit = TEMP_CELSIUS def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Controller/Location.""" From b4d2c25f8e9cea621ec2d2f78373fd9d8a610631 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 23:49:22 +0200 Subject: [PATCH 748/820] Raise YAML removal issue for Xbox (#75843) --- homeassistant/components/xbox/__init__.py | 12 +++++++++++- homeassistant/components/xbox/manifest.json | 2 +- homeassistant/components/xbox/strings.json | 6 ++++++ homeassistant/components/xbox/translations/en.json | 6 ++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 6e8492e45ca..c49fd55e8c8 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -21,6 +21,7 @@ from xbox.webapi.api.provider.smartglass.models import ( ) from homeassistant.components import application_credentials +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant @@ -74,9 +75,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET] ), ) + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( "Configuration of Xbox integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " + "will be removed in Home Assistant 2022.9.; Your existing configuration " "(including OAuth Application Credentials) has been imported into " "the UI automatically and can be safely removed from your " "configuration.yaml file" diff --git a/homeassistant/components/xbox/manifest.json b/homeassistant/components/xbox/manifest.json index 5adfa54a901..8857a55d66d 100644 --- a/homeassistant/components/xbox/manifest.json +++ b/homeassistant/components/xbox/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xbox", "requirements": ["xbox-webapi==2.0.11"], - "dependencies": ["auth", "application_credentials"], + "dependencies": ["auth", "application_credentials", "repairs"], "codeowners": ["@hunterjm"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/xbox/strings.json b/homeassistant/components/xbox/strings.json index accd6775941..68af0176fa8 100644 --- a/homeassistant/components/xbox/strings.json +++ b/homeassistant/components/xbox/strings.json @@ -13,5 +13,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Xbox YAML configuration is being removed", + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/xbox/translations/en.json b/homeassistant/components/xbox/translations/en.json index 0bb1266bded..2ef065af458 100644 --- a/homeassistant/components/xbox/translations/en.json +++ b/homeassistant/components/xbox/translations/en.json @@ -13,5 +13,11 @@ "title": "Pick Authentication Method" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Xbox YAML configuration is being removed" + } } } \ No newline at end of file From 2dc318be54f00a610e7007a041bab2cdf09acbb1 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 28 Jul 2022 12:05:56 +0300 Subject: [PATCH 749/820] Add issue to repairs for deprecated Simplepush YAML configuration (#75850) --- homeassistant/components/simplepush/manifest.json | 1 + homeassistant/components/simplepush/notify.py | 12 ++++++++++++ homeassistant/components/simplepush/strings.json | 6 ++++++ .../components/simplepush/translations/en.json | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index 7c37546485a..6b4ee263ba6 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -5,6 +5,7 @@ "requirements": ["simplepush==1.1.4"], "codeowners": ["@engrbm87"], "config_flow": true, + "dependencies": ["repairs"], "iot_class": "cloud_polling", "loggers": ["simplepush"] } diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index e9cd9813175..358d95c770a 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -14,6 +14,8 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.components.notify.const import ATTR_DATA +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_EVENT, CONF_PASSWORD from homeassistant.core import HomeAssistant @@ -41,6 +43,16 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + if discovery_info is None: hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/simplepush/strings.json b/homeassistant/components/simplepush/strings.json index 0031dc32340..77ed05c4b48 100644 --- a/homeassistant/components/simplepush/strings.json +++ b/homeassistant/components/simplepush/strings.json @@ -17,5 +17,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index a36a3b2b273..bf373d8baf0 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } \ No newline at end of file From c10ed6edbaae83e1dffec0796ae53794dcea3ee8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 12:39:10 +0200 Subject: [PATCH 750/820] Fix unit of measurement usage in COSignal (#75856) --- homeassistant/components/co2signal/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index f7514664698..841848621ec 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -103,8 +103,8 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" - if self.entity_description.unit_of_measurement: - return self.entity_description.unit_of_measurement + if self.entity_description.native_unit_of_measurement: + return self.entity_description.native_unit_of_measurement return cast( str, self.coordinator.data["units"].get(self.entity_description.key) ) From a000687eb58abf69004d17aa24777b37db3a8aa9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 16:43:32 +0200 Subject: [PATCH 751/820] Fix HTTP 404 being logged as a stack trace (#75861) --- homeassistant/components/http/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index c4dc97727a9..6cb1bafdaca 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -33,7 +33,7 @@ def _get_file_path( return None if filepath.is_file(): return filepath - raise HTTPNotFound + raise FileNotFoundError class CachingStaticResource(StaticResource): From 4be623a4922681f8cbc491cef81115f0dbda4ca3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:13:16 +0200 Subject: [PATCH 752/820] Remove state class from daily net sensors in DSMR Reader (#75864) --- homeassistant/components/dsmr_reader/definitions.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 9f4ee7ed918..ac61837afec 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -225,42 +225,36 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Low tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2", name="High tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_returned", name="Low tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2_returned", name="High tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_merged", name="Power usage total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_returned_merged", name="Power return total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_cost", From 2bf10799ed68c96a86d16812922da53e8271451a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:15:27 +0200 Subject: [PATCH 753/820] Fix incorrect sensor key in DSMR (#75865) --- homeassistant/components/dsmr/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 7ab1a3bb45b..aa01c798072 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -85,7 +85,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key="electricity_delivery", + key="current_electricity_delivery", name="Power production", obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, device_class=SensorDeviceClass.POWER, From f98d95c76f30eb0bbcdeab72d0a931b8971ec9b8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:20:39 +0200 Subject: [PATCH 754/820] Fix camera token to trigger authentication IP ban (#75870) --- homeassistant/components/camera/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 247f73c89f2..3bf86dedea1 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -715,7 +715,9 @@ class CameraView(HomeAssistantView): ) if not authenticated: - raise web.HTTPUnauthorized() + if request[KEY_AUTHENTICATED]: + raise web.HTTPUnauthorized() + raise web.HTTPForbidden() if not camera.is_on: _LOGGER.debug("Camera is off") From 38909855bfe811275245b28b4a1e31877c58bea3 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 28 Jul 2022 11:20:10 -0500 Subject: [PATCH 755/820] Update frontend to 20220728.0 (#75872) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2a1fb2d3b37..45331491aa0 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220727.0"], + "requirements": ["home-assistant-frontend==20220728.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2554f0185b4..1f86ff4c7c5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 9c48e9984eb..24a8a5ee1bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f650a56a30..81e192ce360 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 53870dd0bce72dd2e65736028f57aac3d05ab51c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 19:09:46 +0200 Subject: [PATCH 756/820] Bumped version to 2022.8.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 02896fcbe96..dfa12ccd824 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 5306a589863..f5b35dd63dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b0" +version = "2022.8.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e4e36b51b6e54f2e11554433944de44ca0c30db4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jul 2022 11:14:13 -1000 Subject: [PATCH 757/820] Add startup timeout to bluetooth (#75848) Co-authored-by: Martin Hjelmare --- .../components/bluetooth/__init__.py | 11 +++++++- tests/components/bluetooth/test_init.py | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 551e93d5bd9..d2bdb54d5ba 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,6 +1,7 @@ """The bluetooth integration.""" from __future__ import annotations +import asyncio from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta @@ -8,6 +9,7 @@ from enum import Enum import logging from typing import Final, Union +import async_timeout from bleak import BleakError from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData @@ -43,6 +45,7 @@ _LOGGER = logging.getLogger(__name__) UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 +START_TIMEOUT = 15 SOURCE_LOCAL: Final = "local" @@ -300,7 +303,13 @@ class BluetoothManager: self._device_detected, {} ) try: - await self.scanner.start() + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() + except asyncio.TimeoutError as ex: + self._cancel_device_detected() + raise ConfigEntryNotReady( + f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex except (FileNotFoundError, BleakError) as ex: self._cancel_device_detected() raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index bb2c5f49cc9..66a9ed396ec 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" +import asyncio from datetime import timedelta from unittest.mock import MagicMock, patch @@ -95,6 +96,33 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): assert len(bluetooth.async_discovered_service_info(hass)) == 0 +async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): + """Test we fail gracefully when bluetooth/dbus is hanging.""" + mock_bt = [] + + async def _mock_hang(): + await asyncio.sleep(1) + + with patch.object(bluetooth, "START_TIMEOUT", 0), patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=_mock_hang, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "Timed out starting Bluetooth" in caplog.text + + async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): """Test we retry if the adapter is not yet available.""" mock_bt = [] From 2b1fbbfae3b55f300a892f178fa9e4b285254f2d Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 28 Jul 2022 19:10:37 +0100 Subject: [PATCH 758/820] Fix Xiaomi BLE not detecting encryption for some devices (#75851) --- .../components/bluetooth/__init__.py | 29 +++++ .../components/xiaomi_ble/config_flow.py | 44 ++++++- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_init.py | 89 ++++++++++++- tests/components/xiaomi_ble/__init__.py | 12 ++ .../components/xiaomi_ble/test_config_flow.py | 123 ++++++++++++++++++ 8 files changed, 297 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index d2bdb54d5ba..eb8e31baef0 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from asyncio import Future from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta @@ -98,6 +99,9 @@ BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[ [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None ] +ProcessAdvertisementCallback = Callable[ + [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo]], bool +] @hass_callback @@ -162,6 +166,31 @@ def async_register_callback( return manager.async_register_callback(callback, match_dict) +async def async_process_advertisements( + hass: HomeAssistant, + callback: ProcessAdvertisementCallback, + match_dict: BluetoothCallbackMatcher, + timeout: int, +) -> BluetoothServiceInfo: + """Process advertisements until callback returns true or timeout expires.""" + done: Future[BluetoothServiceInfo] = Future() + + @hass_callback + def _async_discovered_device( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + if callback(service_info): + done.set_result(service_info) + + unload = async_register_callback(hass, _async_discovered_device, match_dict) + + try: + async with async_timeout.timeout(timeout): + return await done + finally: + unload() + + @hass_callback def async_track_unavailable( hass: HomeAssistant, diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index e7c4a3e1f8c..f352f43d0bf 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Xiaomi Bluetooth integration.""" from __future__ import annotations +import asyncio import dataclasses from typing import Any @@ -12,6 +13,7 @@ from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothServiceInfo, async_discovered_service_info, + async_process_advertisements, ) from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ADDRESS @@ -19,6 +21,9 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN +# How long to wait for additional advertisement packets if we don't have the right ones +ADDITIONAL_DISCOVERY_TIMEOUT = 5 + @dataclasses.dataclass class Discovery: @@ -44,6 +49,24 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} + async def _async_wait_for_full_advertisement( + self, discovery_info: BluetoothServiceInfo, device: DeviceData + ) -> BluetoothServiceInfo: + """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" + if not device.pending: + return discovery_info + + def _process_more_advertisements(service_info: BluetoothServiceInfo) -> bool: + device.update(service_info) + return not device.pending + + return await async_process_advertisements( + self.hass, + _process_more_advertisements, + {"address": discovery_info.address}, + ADDITIONAL_DISCOVERY_TIMEOUT, + ) + async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo ) -> FlowResult: @@ -53,6 +76,16 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): device = DeviceData() if not device.supported(discovery_info): return self.async_abort(reason="not_supported") + + # Wait until we have received enough information about this device to detect its encryption type + try: + discovery_info = await self._async_wait_for_full_advertisement( + discovery_info, device + ) + except asyncio.TimeoutError: + # If we don't see a valid packet within the timeout then this device is not supported. + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info self._discovered_device = device @@ -161,13 +194,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) discovery = self._discovered_devices[address] + # Wait until we have received enough information about this device to detect its encryption type + try: + self._discovery_info = await self._async_wait_for_full_advertisement( + discovery.discovery_info, discovery.device + ) + except asyncio.TimeoutError: + # If we don't see a valid packet within the timeout then this device is not supported. + return self.async_abort(reason="not_supported") + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: - self._discovery_info = discovery.discovery_info self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_legacy() if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: - self._discovery_info = discovery.discovery_info self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_4_5() diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 2e1a502ca69..41512291749 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.1"], + "requirements": ["xiaomi-ble==0.6.2"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 24a8a5ee1bd..ed487c9b84c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2470,7 +2470,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.1 +xiaomi-ble==0.6.2 # homeassistant.components.knx xknx==0.22.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81e192ce360..73b2ad3adc0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.1 +xiaomi-ble==0.6.2 # homeassistant.components.knx xknx==0.22.0 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 66a9ed396ec..0664f82dbab 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -13,6 +13,7 @@ from homeassistant.components.bluetooth import ( UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, + async_process_advertisements, async_track_unavailable, models, ) @@ -22,7 +23,7 @@ from homeassistant.components.bluetooth.const import ( ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -819,6 +820,92 @@ async def test_register_callback_by_address( assert service_info.manufacturer_id == 89 +async def test_process_advertisements_bail_on_good_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Test as soon as we see a 'good' advertisement we return it.""" + done = asyncio.Future() + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set_result(None) + return len(service_info.service_data) > 0 + + handle = hass.async_create_task( + async_process_advertisements( + hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + ) + ) + + while not done.done(): + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.name == "wohand" + + +async def test_process_advertisements_ignore_bad_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Check that we ignore bad advertisements.""" + done = asyncio.Event() + return_value = asyncio.Event() + + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""}, + ) + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set() + return return_value.is_set() + + handle = hass.async_create_task( + async_process_advertisements( + hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + ) + ) + + # The goal of this loop is to make sure that async_process_advertisements sees at least one + # callback that returns False + while not done.is_set(): + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + # Set the return value and mutate the advertisement + # Check that scan ends and correct advertisement data is returned + return_value.set() + adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c" + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.service_data["00000d00-0000-1000-8000-00805f9b34fa"] == b"H\x10c" + + +async def test_process_advertisements_timeout( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test we timeout if no advertisements at all.""" + + def _callback(service_info: BluetoothServiceInfo) -> bool: + return False + + with pytest.raises(asyncio.TimeoutError): + await async_process_advertisements(hass, _callback, {}, 0) + + async def test_wrapped_instance_with_filter( hass, mock_bleak_scanner_start, enable_bluetooth ): diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index a6269a02d12..1dd1eeed65a 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -61,6 +61,18 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( source="local", ) +MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfo( + name="LYWSD02MMC", + address="A4:C1:38:56:53:84", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"0X[\x05\x02\x84\x53\x568\xc1\xa4\x08", + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: """Make a dummy advertisement.""" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index d4b4300d2c1..b424228cc6c 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Xiaomi config flow.""" +import asyncio from unittest.mock import patch from homeassistant import config_entries @@ -9,9 +10,11 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( JTYJGD03MI_SERVICE_INFO, LYWSDCGQ_SERVICE_INFO, + MISSING_PAYLOAD_ENCRYPTED, MMC_T201_1_SERVICE_INFO, NOT_SENSOR_PUSH_SERVICE_INFO, YLKG07YL_SERVICE_INFO, + make_advertisement, ) from tests.common import MockConfigEntry @@ -38,6 +41,57 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" +async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): + """Test discovery via bluetooth with a valid device but missing payload.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): + """Test discovering a valid device. Payload is too short, but later we get full one.""" + + async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" + + async def test_async_step_bluetooth_during_onboarding(hass): """Test discovery via bluetooth during onboarding.""" with patch( @@ -287,6 +341,75 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "58:2D:34:35:93:21" +async def test_async_step_user_short_payload(hass): + """Test setup from service info cache with devices found but short payloads.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "not_supported" + + +async def test_async_step_user_short_payload_then_full(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + + async def test_async_step_user_with_found_devices_v4_encryption(hass): """Test setup from service info cache with devices found, with v4 encryption.""" with patch( From c469bdea75badecf2bfbf4bc43260ae0cc1fccb2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 22:36:11 +0200 Subject: [PATCH 759/820] Fix AdGuard Home rules count sensor (#75879) --- homeassistant/components/adguard/sensor.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 07a483f03c4..86104d15ef2 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -26,7 +26,7 @@ PARALLEL_UPDATES = 4 class AdGuardHomeEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, int | float]]] + value_fn: Callable[[AdGuardHome], Coroutine[Any, Any, int | float]] @dataclass @@ -42,56 +42,56 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( name="DNS queries", icon="mdi:magnify", native_unit_of_measurement="queries", - value_fn=lambda adguard: adguard.stats.dns_queries, + value_fn=lambda adguard: adguard.stats.dns_queries(), ), AdGuardHomeEntityDescription( key="blocked_filtering", name="DNS queries blocked", icon="mdi:magnify-close", native_unit_of_measurement="queries", - value_fn=lambda adguard: adguard.stats.blocked_filtering, + value_fn=lambda adguard: adguard.stats.blocked_filtering(), ), AdGuardHomeEntityDescription( key="blocked_percentage", name="DNS queries blocked ratio", icon="mdi:magnify-close", native_unit_of_measurement=PERCENTAGE, - value_fn=lambda adguard: adguard.stats.blocked_percentage, + value_fn=lambda adguard: adguard.stats.blocked_percentage(), ), AdGuardHomeEntityDescription( key="blocked_parental", name="Parental control blocked", icon="mdi:human-male-girl", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_parental, + value_fn=lambda adguard: adguard.stats.replaced_parental(), ), AdGuardHomeEntityDescription( key="blocked_safebrowsing", name="Safe browsing blocked", icon="mdi:shield-half-full", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_safebrowsing, + value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(), ), AdGuardHomeEntityDescription( key="enforced_safesearch", name="Safe searches enforced", icon="mdi:shield-search", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_safesearch, + value_fn=lambda adguard: adguard.stats.replaced_safesearch(), ), AdGuardHomeEntityDescription( key="average_speed", name="Average processing speed", icon="mdi:speedometer", native_unit_of_measurement=TIME_MILLISECONDS, - value_fn=lambda adguard: adguard.stats.avg_processing_time, + value_fn=lambda adguard: adguard.stats.avg_processing_time(), ), AdGuardHomeEntityDescription( key="rules_count", name="Rules count", icon="mdi:counter", native_unit_of_measurement="rules", - value_fn=lambda adguard: adguard.stats.avg_processing_time, + value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False), entity_registry_enabled_default=False, ), ) @@ -144,7 +144,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - value = await self.entity_description.value_fn(self.adguard)() + value = await self.entity_description.value_fn(self.adguard) self._attr_native_value = value if isinstance(value, float): self._attr_native_value = f"{value:.2f}" From 97c6c949e71a1978f87ac79abe74fd0fdd5c2418 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jul 2022 17:07:32 -1000 Subject: [PATCH 760/820] Fix incorrect manufacturer_id for govee 5182 model (#75899) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index aa86215da59..270858d04d4 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -18,9 +18,13 @@ { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.3"], + "requirements": ["govee-ble==0.12.4"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 5dde90f1f7a..8d92d6eab4a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -34,6 +34,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" + }, { "domain": "homekit_controller", "manufacturer_id": 76, diff --git a/requirements_all.txt b/requirements_all.txt index ed487c9b84c..3bfd3880eee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.3 +govee-ble==0.12.4 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73b2ad3adc0..6ce69ed9678 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.3 +govee-ble==0.12.4 # homeassistant.components.gree greeclimate==1.2.0 From dfd503cc1aaf17aa7e95f6950814fec11a710dff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jul 2022 00:53:08 -0700 Subject: [PATCH 761/820] Fix Roon media player being set up before hass.data set up (#75904) --- homeassistant/components/roon/__init__.py | 11 ++++++++++- homeassistant/components/roon/server.py | 9 +-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index 9e5c38f0211..9969b694895 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -1,12 +1,14 @@ """Roon (www.roonlabs.com) component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .const import CONF_ROON_NAME, DOMAIN from .server import RoonServer +PLATFORMS = [Platform.MEDIA_PLAYER] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a roonserver from a config entry.""" @@ -28,10 +30,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manufacturer="Roonlabs", name=f"Roon Core ({name})", ) + + # initialize media_player platform + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + return False + roonserver = hass.data[DOMAIN].pop(entry.entry_id) return await roonserver.async_reset() diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index df9dec3d9af..997db44583d 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -4,7 +4,7 @@ import logging from roonapi import RoonApi, RoonDiscovery -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, Platform +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.dt import utcnow @@ -13,7 +13,6 @@ from .const import CONF_ROON_ID, ROON_APPINFO _LOGGER = logging.getLogger(__name__) INITIAL_SYNC_INTERVAL = 5 FULL_SYNC_INTERVAL = 30 -PLATFORMS = [Platform.MEDIA_PLAYER] class RoonServer: @@ -53,7 +52,6 @@ class RoonServer: (host, port) = get_roon_host() return RoonApi(ROON_APPINFO, token, host, port, blocking_init=True) - hass = self.hass core_id = self.config_entry.data.get(CONF_ROON_ID) self.roonapi = await self.hass.async_add_executor_job(get_roon_api) @@ -67,11 +65,6 @@ class RoonServer: core_id if core_id is not None else self.config_entry.data[CONF_HOST] ) - # initialize media_player platform - await hass.config_entries.async_forward_entry_setups( - self.config_entry, PLATFORMS - ) - # Initialize Roon background polling asyncio.create_task(self.async_do_loop()) From f4defb660be0f407bb48d7f0c9fbebdc4544bb96 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 29 Jul 2022 11:57:19 +0200 Subject: [PATCH 762/820] Fix broken Yale lock (#75918) Yale fix lock --- homeassistant/components/yale_smart_alarm/lock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index a97a98a2afb..8f9ed6c9ce1 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -46,6 +46,7 @@ class YaleDoorlock(YaleEntity, LockEntity): """Initialize the Yale Lock Device.""" super().__init__(coordinator, data) self._attr_code_format = f"^\\d{code_format}$" + self.lock_name = data["name"] async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" @@ -65,7 +66,7 @@ class YaleDoorlock(YaleEntity, LockEntity): try: get_lock = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.get, self._attr_name + self.coordinator.yale.lock_api.get, self.lock_name ) if command == "locked": lock_state = await self.hass.async_add_executor_job( From 48b97a1f2dcaaa9483207774cfadb8f2ee820024 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Jul 2022 13:41:22 +0200 Subject: [PATCH 763/820] Bumped version to 2022.8.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dfa12ccd824..a3753d36d07 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index f5b35dd63dc..558f366b357 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b1" +version = "2022.8.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 176d44190e01dcdd3b201c541dea5fbb04b7fef7 Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Fri, 29 Jul 2022 19:11:53 +0200 Subject: [PATCH 764/820] Fix incorrect check for media source (#75880) * Fix incorrect check for media source * Update homeassistant/components/jellyfin/media_source.py Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- homeassistant/components/jellyfin/media_source.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 879f4a4d4c8..8a09fd8d552 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -363,14 +363,18 @@ class JellyfinSource(MediaSource): def _media_mime_type(media_item: dict[str, Any]) -> str: """Return the mime type of a media item.""" - if not media_item[ITEM_KEY_MEDIA_SOURCES]: + if not media_item.get(ITEM_KEY_MEDIA_SOURCES): raise BrowseError("Unable to determine mime type for item without media source") media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] + + if MEDIA_SOURCE_KEY_PATH not in media_source: + raise BrowseError("Unable to determine mime type for media source without path") + path = media_source[MEDIA_SOURCE_KEY_PATH] mime_type, _ = mimetypes.guess_type(path) - if mime_type is not None: - return mime_type + if mime_type is None: + raise BrowseError(f"Unable to determine mime type for path {path}") - raise BrowseError(f"Unable to determine mime type for path {path}") + return mime_type From d7827d99020c77c986905637928d0f0e6dd7f6c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Jul 2022 18:56:19 +0200 Subject: [PATCH 765/820] Fix SimplePush repairs issue (#75922) --- homeassistant/components/simplepush/notify.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 358d95c770a..2e58748f323 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -43,17 +43,16 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2022.9.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - if discovery_info is None: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config From c3c5442467be7179f4d5c3c8efc3f975ead576a8 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 29 Jul 2022 13:28:39 +0100 Subject: [PATCH 766/820] Fix xiaomi_ble discovery for devices that don't put the fe95 uuid in service_uuids (#75923) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- homeassistant/generated/bluetooth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 41512291749..0d97dcbedf8 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "bluetooth": [ { - "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], "requirements": ["xiaomi-ble==0.6.2"], diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 8d92d6eab4a..2cbaebb6074 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -80,6 +80,6 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ }, { "domain": "xiaomi_ble", - "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ] From 241ffe07b977c245abb1849817fa62651618d6b2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 29 Jul 2022 18:54:49 +0200 Subject: [PATCH 767/820] Update xknx to 0.22.1 (#75932) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 4197cb76209..266eceaacee 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.22.0"], + "requirements": ["xknx==0.22.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 3bfd3880eee..b7b3ed89b27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2473,7 +2473,7 @@ xboxapi==2.0.1 xiaomi-ble==0.6.2 # homeassistant.components.knx -xknx==0.22.0 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ce69ed9678..d04b537c9fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.6.2 # homeassistant.components.knx -xknx==0.22.0 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz From 0f0b51bee71ff87a39e9895645f6bfca236644cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Jul 2022 22:26:37 -0700 Subject: [PATCH 768/820] Move some bleak imports to be behind TYPE_CHECKING (#75894) --- .../components/bluetooth_le_tracker/device_tracker.py | 5 ++++- homeassistant/components/fjaraskupan/__init__.py | 8 ++++++-- homeassistant/components/switchbot/coordinator.py | 10 ++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index f85cc2bad0a..e42d19ac547 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -5,10 +5,10 @@ import asyncio from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging +from typing import TYPE_CHECKING from uuid import UUID from bleak import BleakClient, BleakError -from bleak.backends.device import BLEDevice import voluptuous as vol from homeassistant.components import bluetooth @@ -31,6 +31,9 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + _LOGGER = logging.getLogger(__name__) # Base UUID: 00000000-0000-1000-8000-00805F9B34FB diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 85e95db5513..36608fb026d 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -5,10 +5,9 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging +from typing import TYPE_CHECKING from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry @@ -24,6 +23,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DISPATCH_DETECTION, DOMAIN +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + PLATFORMS = [ Platform.BINARY_SENSOR, Platform.FAN, diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 31f7f2d3992..f461a3e0f4c 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -3,11 +3,9 @@ from __future__ import annotations import asyncio import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast -from bleak.backends.device import BLEDevice import switchbot -from switchbot import parse_advertisement_data from homeassistant.components import bluetooth from homeassistant.components.bluetooth.passive_update_coordinator import ( @@ -15,6 +13,10 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( ) from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + _LOGGER = logging.getLogger(__name__) @@ -52,7 +54,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): """Handle a Bluetooth event.""" super()._async_handle_bluetooth_event(service_info, change) discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) - if adv := parse_advertisement_data( + if adv := switchbot.parse_advertisement_data( discovery_info_bleak.device, discovery_info_bleak.advertisement ): self.data = flatten_sensors_data(adv.data) From 26c475d3dc51f39a588c540021a7f8f19f2f84ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jul 2022 14:53:33 -1000 Subject: [PATCH 769/820] Update to bleak 0.15 (#75941) --- .../components/bluetooth/__init__.py | 29 ++++++++------- .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/models.py | 34 ++++++++++++----- .../bluetooth/passive_update_coordinator.py | 8 ++-- .../bluetooth/passive_update_processor.py | 17 +++++---- .../bluetooth/update_coordinator.py | 9 +++-- homeassistant/components/bluetooth/usage.py | 5 ++- .../bluetooth_le_tracker/device_tracker.py | 18 ++++----- .../components/govee_ble/__init__.py | 2 + .../components/govee_ble/config_flow.py | 6 +-- .../homekit_controller/config_flow.py | 7 +++- .../components/homekit_controller/utils.py | 2 +- homeassistant/components/inkbird/__init__.py | 5 +-- .../components/inkbird/config_flow.py | 6 +-- homeassistant/components/moat/__init__.py | 5 +-- homeassistant/components/moat/config_flow.py | 6 +-- .../components/sensorpush/__init__.py | 5 +-- .../components/sensorpush/config_flow.py | 6 +-- .../components/switchbot/config_flow.py | 8 ++-- .../components/switchbot/coordinator.py | 11 +++--- .../components/xiaomi_ble/__init__.py | 5 +-- .../components/xiaomi_ble/config_flow.py | 20 ++++++---- homeassistant/config_entries.py | 4 +- homeassistant/helpers/config_entry_flow.py | 4 +- .../helpers/service_info/bluetooth.py | 1 + homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_init.py | 21 +++++++++-- .../test_passive_update_coordinator.py | 29 ++++++++++----- .../test_passive_update_processor.py | 37 ++++++++++--------- .../test_device_tracker.py | 23 +++++++++--- tests/components/fjaraskupan/conftest.py | 6 +++ tests/components/govee_ble/test_sensor.py | 2 +- tests/components/inkbird/test_sensor.py | 2 +- tests/components/moat/test_sensor.py | 2 +- tests/components/sensorpush/test_sensor.py | 2 +- .../components/xiaomi_ble/test_config_flow.py | 8 +++- tests/components/xiaomi_ble/test_sensor.py | 12 +++--- 39 files changed, 223 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index eb8e31baef0..9adaac84333 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -from typing import Final, Union +from typing import Final import async_timeout from bleak import BleakError @@ -96,12 +96,8 @@ SCANNING_MODE_TO_BLEAK = { BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") -BluetoothCallback = Callable[ - [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None -] -ProcessAdvertisementCallback = Callable[ - [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo]], bool -] +BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] +ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] @hass_callback @@ -157,9 +153,15 @@ def async_register_callback( hass: HomeAssistant, callback: BluetoothCallback, match_dict: BluetoothCallbackMatcher | None, + mode: BluetoothScanningMode, ) -> Callable[[], None]: """Register to receive a callback on bluetooth change. + mode is currently not used as we only support active scanning. + Passive scanning will be available in the future. The flag + is required to be present to avoid a future breaking change + when we support passive scanning. + Returns a callback that can be used to cancel the registration. """ manager: BluetoothManager = hass.data[DOMAIN] @@ -170,19 +172,20 @@ async def async_process_advertisements( hass: HomeAssistant, callback: ProcessAdvertisementCallback, match_dict: BluetoothCallbackMatcher, + mode: BluetoothScanningMode, timeout: int, -) -> BluetoothServiceInfo: +) -> BluetoothServiceInfoBleak: """Process advertisements until callback returns true or timeout expires.""" - done: Future[BluetoothServiceInfo] = Future() + done: Future[BluetoothServiceInfoBleak] = Future() @hass_callback def _async_discovered_device( - service_info: BluetoothServiceInfo, change: BluetoothChange + service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: if callback(service_info): done.set_result(service_info) - unload = async_register_callback(hass, _async_discovered_device, match_dict) + unload = async_register_callback(hass, _async_discovered_device, match_dict, mode) try: async with async_timeout.timeout(timeout): @@ -333,7 +336,7 @@ class BluetoothManager: ) try: async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() + await self.scanner.start() # type: ignore[no-untyped-call] except asyncio.TimeoutError as ex: self._cancel_device_detected() raise ConfigEntryNotReady( @@ -500,7 +503,7 @@ class BluetoothManager: self._cancel_unavailable_tracking = None if self.scanner: try: - await self.scanner.stop() + await self.scanner.stop() # type: ignore[no-untyped-call] except BleakError as ex: # This is not fatal, and they may want to reload # the config entry to restart the scanner if they diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index c6ca8b11400..f215e8fa161 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.2"], + "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.2"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 408e0698879..6f814c7b66b 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import Any, Final, cast +from typing import Any, Final from bleak import BleakScanner from bleak.backends.device import BLEDevice @@ -32,7 +32,7 @@ def _dispatch_callback( """Dispatch the callback.""" if not callback: # Callback destroyed right before being called, ignore - return + return # type: ignore[unreachable] if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( advertisement_data.service_uuids @@ -45,7 +45,7 @@ def _dispatch_callback( _LOGGER.exception("Error in callback: %s", callback) -class HaBleakScanner(BleakScanner): # type: ignore[misc] +class HaBleakScanner(BleakScanner): """BleakScanner that cannot be stopped.""" def __init__( # pylint: disable=super-init-not-called @@ -106,16 +106,29 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] _dispatch_callback(*callback_filters, device, advertisement_data) -class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] +class HaBleakScannerWrapper(BaseBleakScanner): """A wrapper that uses the single instance.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__( + self, + *args: Any, + detection_callback: AdvertisementDataCallback | None = None, + service_uuids: list[str] | None = None, + **kwargs: Any, + ) -> None: """Initialize the BleakScanner.""" self._detection_cancel: CALLBACK_TYPE | None = None self._mapped_filters: dict[str, set[str]] = {} self._adv_data_callback: AdvertisementDataCallback | None = None - self._map_filters(*args, **kwargs) - super().__init__(*args, **kwargs) + remapped_kwargs = { + "detection_callback": detection_callback, + "service_uuids": service_uuids or [], + **kwargs, + } + self._map_filters(*args, **remapped_kwargs) + super().__init__( + detection_callback=detection_callback, service_uuids=service_uuids or [] + ) async def stop(self, *args: Any, **kwargs: Any) -> None: """Stop scanning for devices.""" @@ -153,9 +166,11 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" assert HA_BLEAK_SCANNER is not None - return cast(list[BLEDevice], HA_BLEAK_SCANNER.discovered_devices) + return HA_BLEAK_SCANNER.discovered_devices - def register_detection_callback(self, callback: AdvertisementDataCallback) -> None: + def register_detection_callback( + self, callback: AdvertisementDataCallback | None + ) -> None: """Register a callback that is called when a device is discovered or has a property changed. This method takes the callback and registers it with the long running @@ -171,6 +186,7 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] self._cancel_callback() super().register_detection_callback(self._adv_data_callback) assert HA_BLEAK_SCANNER is not None + assert self._callback is not None self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( self._callback, self._mapped_filters ) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 97e7ddc49ee..31a6b065830 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -6,10 +6,9 @@ import logging from typing import Any from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BluetoothChange +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .update_coordinator import BasePassiveBluetoothCoordinator @@ -25,9 +24,10 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize PassiveBluetoothDataUpdateCoordinator.""" - super().__init__(hass, logger, address) + super().__init__(hass, logger, address, mode) self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} @callback @@ -65,7 +65,7 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 43467701879..1f2047c02cb 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -6,14 +6,12 @@ import dataclasses import logging from typing import Any, Generic, TypeVar -from home_assistant_bluetooth import BluetoothServiceInfo - from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BluetoothChange +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator @@ -62,9 +60,10 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize the coordinator.""" - super().__init__(hass, logger, address) + super().__init__(hass, logger, address, mode) self._processors: list[PassiveBluetoothDataProcessor] = [] @callback @@ -92,7 +91,7 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" @@ -122,7 +121,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): The processor will call the update_method every time the bluetooth device receives a new advertisement data from the coordinator with the following signature: - update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + update_method(service_info: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate As the size of each advertisement is limited, the update_method should return a PassiveBluetoothDataUpdate object that contains only data that @@ -135,7 +134,9 @@ class PassiveBluetoothDataProcessor(Generic[_T]): def __init__( self, - update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + update_method: Callable[ + [BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T] + ], ) -> None: """Initialize the coordinator.""" self.coordinator: PassiveBluetoothProcessorCoordinator @@ -241,7 +242,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): @callback def async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index b1cb2de1453..d0f38ce32c6 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -4,13 +4,13 @@ from __future__ import annotations import logging import time -from home_assistant_bluetooth import BluetoothServiceInfo - from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from . import ( BluetoothCallbackMatcher, BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, async_register_callback, async_track_unavailable, ) @@ -27,6 +27,7 @@ class BasePassiveBluetoothCoordinator: hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize the coordinator.""" self.hass = hass @@ -36,6 +37,7 @@ class BasePassiveBluetoothCoordinator: self._cancel_track_unavailable: CALLBACK_TYPE | None = None self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None self._present = False + self.mode = mode self.last_seen = 0.0 @callback @@ -61,6 +63,7 @@ class BasePassiveBluetoothCoordinator: self.hass, self._async_handle_bluetooth_event, BluetoothCallbackMatcher(address=self.address), + self.mode, ) self._cancel_track_unavailable = async_track_unavailable( self.hass, @@ -86,7 +89,7 @@ class BasePassiveBluetoothCoordinator: @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index da5d062a36f..b3a6783cf30 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -1,4 +1,5 @@ """bluetooth usage utility to handle multiple instances.""" + from __future__ import annotations import bleak @@ -10,9 +11,9 @@ ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" - bleak.BleakScanner = HaBleakScannerWrapper + bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] def uninstall_multiple_bleak_catcher() -> None: """Unwrap the bleak classes.""" - bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER + bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index e42d19ac547..01b3d48205a 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -5,7 +5,6 @@ import asyncio from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING from uuid import UUID from bleak import BleakClient, BleakError @@ -31,9 +30,6 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - _LOGGER = logging.getLogger(__name__) # Base UUID: 00000000-0000-1000-8000-00805F9B34FB @@ -139,15 +135,12 @@ async def async_setup_scanner( # noqa: C901 async def _async_see_update_ble_battery( mac: str, now: datetime, - service_info: bluetooth.BluetoothServiceInfo, + service_info: bluetooth.BluetoothServiceInfoBleak, ) -> None: """Lookup Bluetooth LE devices and update status.""" battery = None - ble_device: BLEDevice | str = ( - bluetooth.async_ble_device_from_address(hass, mac) or mac - ) try: - async with BleakClient(ble_device) as client: + async with BleakClient(service_info.device) as client: bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) battery = ord(bat_char) except asyncio.TimeoutError: @@ -168,7 +161,8 @@ async def async_setup_scanner( # noqa: C901 @callback def _async_update_ble( - service_info: bluetooth.BluetoothServiceInfo, change: bluetooth.BluetoothChange + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, ) -> None: """Update from a ble callback.""" mac = service_info.address @@ -202,7 +196,9 @@ async def async_setup_scanner( # noqa: C901 _async_update_ble(service_info, bluetooth.BluetoothChange.ADVERTISEMENT) cancels = [ - bluetooth.async_register_callback(hass, _async_update_ble, None), + bluetooth.async_register_callback( + hass, _async_update_ble, None, bluetooth.BluetoothScanningMode.ACTIVE + ), async_track_time_interval(hass, _async_refresh_ble, interval), ] diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index 3099d401e9b..7a134e43ace 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -27,6 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, address=address, + mode=BluetoothScanningMode.ACTIVE, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index 9f2efac0ce9..1e3a5566bfd 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -7,7 +7,7 @@ from govee_ble import GoveeBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index d8b3fda2d06..31677e37b20 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -20,13 +20,16 @@ from homeassistant.components import zeroconf from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.service_info import bluetooth from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES from .storage import async_get_entity_storage from .utils import async_get_controller +if TYPE_CHECKING: + from homeassistant.components import bluetooth + + HOMEKIT_DIR = ".homekit" HOMEKIT_BRIDGE_DOMAIN = "homekit" @@ -359,7 +362,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form() async def async_step_bluetooth( - self, discovery_info: bluetooth.BluetoothServiceInfo + self, discovery_info: bluetooth.BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED: diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index d7780029331..6e272067b54 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -32,7 +32,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: controller = Controller( async_zeroconf_instance=async_zeroconf_instance, - bleak_scanner_instance=bleak_scanner_instance, + bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type] ) hass.data[CONTROLLER] = controller diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 5553b1c6ded..0272114b83c 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py index 679ff43b19e..21ed85e117e 100644 --- a/homeassistant/components/inkbird/config_flow.py +++ b/homeassistant/components/inkbird/config_flow.py @@ -7,7 +7,7 @@ from inkbird_ble import INKBIRDBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py index 259b6b66709..237948a8ff6 100644 --- a/homeassistant/components/moat/__init__.py +++ b/homeassistant/components/moat/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py index a353f4963ad..6f51b62d110 100644 --- a/homeassistant/components/moat/config_flow.py +++ b/homeassistant/components/moat/config_flow.py @@ -7,7 +7,7 @@ from moat_ble import MoatBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 0a9efcbc752..d4a0872ba3f 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py index 1a8e8b47abe..d10c2f481a6 100644 --- a/homeassistant/components/sensorpush/config_flow.py +++ b/homeassistant/components/sensorpush/config_flow.py @@ -7,7 +7,7 @@ from sensorpush_ble import SensorPushBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 3a34a89d9fd..eaad573d370 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from switchbot import SwitchBotAdvertisement, parse_advertisement_data import voluptuous as vol @@ -15,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES @@ -46,15 +45,14 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_advs: dict[str, SwitchBotAdvertisement] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) await self.async_set_unique_id(format_unique_id(discovery_info.address)) self._abort_if_unique_id_configured() - discovery_info_bleak = cast(BluetoothServiceInfoBleak, discovery_info) parsed = parse_advertisement_data( - discovery_info_bleak.device, discovery_info_bleak.advertisement + discovery_info.device, discovery_info.advertisement ) if not parsed or parsed.data.get("modelName") not in SUPPORTED_MODEL_TYPES: return self.async_abort(reason="not_supported") diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index f461a3e0f4c..43c576249df 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import logging -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import switchbot @@ -39,7 +39,9 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): device: switchbot.SwitchbotDevice, ) -> None: """Initialize global switchbot data updater.""" - super().__init__(hass, logger, ble_device.address) + super().__init__( + hass, logger, ble_device.address, bluetooth.BluetoothScanningMode.ACTIVE + ) self.ble_device = ble_device self.device = device self.data: dict[str, Any] = {} @@ -48,14 +50,13 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: bluetooth.BluetoothServiceInfo, + service_info: bluetooth.BluetoothServiceInfoBleak, change: bluetooth.BluetoothChange, ) -> None: """Handle a Bluetooth event.""" super()._async_handle_bluetooth_event(service_info, change) - discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) if adv := switchbot.parse_advertisement_data( - discovery_info_bleak.device, discovery_info_bleak.advertisement + service_info.device, service_info.advertisement ): self.data = flatten_sensors_data(adv.data) if "modelName" in self.data: diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 4eb20dbd943..791ac1447ad 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index f352f43d0bf..a2fa2d185dc 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -11,7 +11,8 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothScanningMode, + BluetoothServiceInfoBleak, async_discovered_service_info, async_process_advertisements, ) @@ -30,11 +31,11 @@ class Discovery: """A discovered bluetooth device.""" title: str - discovery_info: BluetoothServiceInfo + discovery_info: BluetoothServiceInfoBleak device: DeviceData -def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: +def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name @@ -45,18 +46,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} async def _async_wait_for_full_advertisement( - self, discovery_info: BluetoothServiceInfo, device: DeviceData - ) -> BluetoothServiceInfo: + self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData + ) -> BluetoothServiceInfoBleak: """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" if not device.pending: return discovery_info - def _process_more_advertisements(service_info: BluetoothServiceInfo) -> bool: + def _process_more_advertisements( + service_info: BluetoothServiceInfoBleak, + ) -> bool: device.update(service_info) return not device.pending @@ -64,11 +67,12 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self.hass, _process_more_advertisements, {"address": discovery_info.address}, + BluetoothScanningMode.ACTIVE, ADDITIONAL_DISCOVERY_TIMEOUT, ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b5d5804f100..b0c04323005 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -27,12 +27,12 @@ from .util import uuid as uuid_util from .util.decorator import Registry if TYPE_CHECKING: + from .components.bluetooth import BluetoothServiceInfoBleak from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo from .components.ssdp import SsdpServiceInfo from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo - from .helpers.service_info.bluetooth import BluetoothServiceInfo from .helpers.service_info.mqtt import MqttServiceInfo _LOGGER = logging.getLogger(__name__) @@ -1485,7 +1485,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Bluetooth discovery.""" return await self._async_step_discovery_without_unique_id() diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index bf2f95c12c6..c9d6ffe6065 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -15,11 +15,11 @@ from .typing import DiscoveryInfoType if TYPE_CHECKING: import asyncio + from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo - from .service_info.bluetooth import BluetoothServiceInfo from .service_info.mqtt import MqttServiceInfo _R = TypeVar("_R", bound="Awaitable[bool] | bool") @@ -97,7 +97,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle a flow initialized by bluetooth discovery.""" if self._async_in_progress() or self._async_current_entries(): diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index 968d1dde95f..0db3a39b114 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -1,4 +1,5 @@ """The bluetooth integration service info.""" + from home_assistant_bluetooth import BluetoothServiceInfo __all__ = ["BluetoothServiceInfo"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1f86ff4c7c5..c9d424daa33 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 -bleak==0.14.3 +bleak==0.15.0 bluetooth-adapters==0.1.2 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b7b3ed89b27..58f2e5f73ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ bimmer_connected==0.10.1 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.14.3 +bleak==0.15.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d04b537c9fd..f7358e148dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ bellows==0.31.2 bimmer_connected==0.10.1 # homeassistant.components.bluetooth -bleak==0.14.3 +bleak==0.15.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 0664f82dbab..a47916506df 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -12,6 +12,7 @@ from homeassistant.components.bluetooth import ( SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, BluetoothServiceInfo, async_process_advertisements, async_track_unavailable, @@ -675,6 +676,7 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo hass, _fake_subscriber, {"service_uuids": {"cba20d00-224d-11e6-9fb8-0002a5d5c51b"}}, + BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 @@ -760,6 +762,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 @@ -799,6 +802,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) cancel() @@ -808,6 +812,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) cancel() @@ -832,7 +837,11 @@ async def test_process_advertisements_bail_on_good_advertisement( handle = hass.async_create_task( async_process_advertisements( - hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, ) ) @@ -873,7 +882,11 @@ async def test_process_advertisements_ignore_bad_advertisement( handle = hass.async_create_task( async_process_advertisements( - hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, ) ) @@ -903,7 +916,9 @@ async def test_process_advertisements_timeout( return False with pytest.raises(asyncio.TimeoutError): - await async_process_advertisements(hass, _callback, {}, 0) + await async_process_advertisements( + hass, _callback, {}, BluetoothScanningMode.ACTIVE, 0 + ) async def test_wrapped_instance_with_filter( diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 4da14fb13d3..31530cd6995 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -10,6 +10,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -42,9 +43,9 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): """An example coordinator that subclasses PassiveBluetoothDataUpdateCoordinator.""" - def __init__(self, hass, logger, device_id) -> None: + def __init__(self, hass, logger, device_id, mode) -> None: """Initialize the coordinator.""" - super().__init__(hass, logger, device_id) + super().__init__(hass, logger, device_id, mode) self.data: dict[str, Any] = {} def _async_handle_bluetooth_event( @@ -60,11 +61,13 @@ class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): async def test_basic_usage(hass, mock_bleak_scanner_start): """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -100,11 +103,13 @@ async def test_context_compatiblity_with_data_update_coordinator( ): """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -149,11 +154,13 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.PASSIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -208,13 +215,15 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start): """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) entity = PassiveBluetoothCoordinatorEntity(coordinator) assert entity.available is False saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index ebb69d7c7d0..6a092746a68 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -16,6 +16,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, ) from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, @@ -90,12 +91,12 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -192,12 +193,12 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -277,12 +278,12 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -336,12 +337,12 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -389,12 +390,12 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -731,12 +732,12 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -841,12 +842,12 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -905,12 +906,12 @@ async def test_passive_bluetooth_entity_with_entity_platform( return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -992,12 +993,12 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index dfe47b38b33..f9f0a51fc0f 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -5,8 +5,9 @@ from datetime import timedelta from unittest.mock import patch from bleak import BleakError +from bleak.backends.scanner import AdvertisementData, BLEDevice -from homeassistant.components.bluetooth import BluetoothServiceInfo +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth_le_tracker import device_tracker from homeassistant.components.bluetooth_le_tracker.device_tracker import ( CONF_TRACK_BATTERY, @@ -79,7 +80,7 @@ async def test_preserve_new_tracked_device_name( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -87,6 +88,8 @@ async def test_preserve_new_tracked_device_name( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -100,7 +103,7 @@ async def test_preserve_new_tracked_device_name( assert result # Seen once here; return without name when seen subsequent times - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=None, address=address, rssi=-19, @@ -108,6 +111,8 @@ async def test_preserve_new_tracked_device_name( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -140,7 +145,7 @@ async def test_tracking_battery_times_out( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -148,6 +153,8 @@ async def test_tracking_battery_times_out( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -202,7 +209,7 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -210,6 +217,8 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -266,7 +275,7 @@ async def test_tracking_battery_successful( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -274,6 +283,8 @@ async def test_tracking_battery_successful( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index d60abcdb9ad..4e06b2ad046 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -17,6 +17,12 @@ def fixture_scanner(hass): class MockScanner(BaseBleakScanner): """Mock Scanner.""" + def __init__(self, *args, **kwargs) -> None: + """Initialize the scanner.""" + super().__init__( + detection_callback=kwargs.pop("detection_callback"), service_uuids=[] + ) + async def start(self): """Start scanning for devices.""" for device in devices: diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index 7a6ecbaed51..75d269ea0ba 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index a851cb92ec3..cafc22911c3 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/moat/test_sensor.py b/tests/components/moat/test_sensor.py index 826bbc72cdb..6424144106b 100644 --- a/tests/components/moat/test_sensor.py +++ b/tests/components/moat/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index 31fbdd8d712..34179985d78 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index b424228cc6c..86fda21aaa0 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -59,7 +59,9 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): """Test discovering a valid device. Payload is too short, but later we get full one.""" - async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): service_info = make_advertisement( "A4:C1:38:56:53:84", b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", @@ -378,7 +380,9 @@ async def test_async_step_user_short_payload_then_full(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): service_info = make_advertisement( "A4:C1:38:56:53:84", b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index dca00e92254..011c6daecae 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -60,7 +60,7 @@ async def test_xiaomi_formaldeyhde(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -107,7 +107,7 @@ async def test_xiaomi_consumable(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -154,7 +154,7 @@ async def test_xiaomi_battery_voltage(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -208,7 +208,7 @@ async def test_xiaomi_HHCCJCY01(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -291,7 +291,7 @@ async def test_xiaomi_CGDK2(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None From 8f8bccd982bf4a60016d98bb084244a251a08773 Mon Sep 17 00:00:00 2001 From: Bob van Mierlo <38190383+bobvmierlo@users.noreply.github.com> Date: Fri, 29 Jul 2022 22:46:30 +0200 Subject: [PATCH 770/820] Increase the discovery timeout (#75948) --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index a2fa2d185dc..aa1ffc24895 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -23,7 +23,7 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN # How long to wait for additional advertisement packets if we don't have the right ones -ADDITIONAL_DISCOVERY_TIMEOUT = 5 +ADDITIONAL_DISCOVERY_TIMEOUT = 60 @dataclasses.dataclass From e2a9ab18311b974a6e60f54e967db18460490c52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jul 2022 17:55:32 -0700 Subject: [PATCH 771/820] Bumped version to 2022.8.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a3753d36d07..4a37902e79f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 558f366b357..8fca49a67f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b2" +version = "2022.8.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 240890e496a156b47eb41b8d0c35287121d11b7c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 31 Jul 2022 14:10:29 -0600 Subject: [PATCH 772/820] Appropriately mark Guardian entities as `unavailable` during reboot (#75234) --- homeassistant/components/guardian/__init__.py | 13 ++++++- .../components/guardian/binary_sensor.py | 2 +- homeassistant/components/guardian/button.py | 3 ++ homeassistant/components/guardian/sensor.py | 4 +- homeassistant/components/guardian/util.py | 38 ++++++++++++++++++- 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index da22066d7aa..58d70667cdf 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -137,6 +137,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # so we use a lock to ensure that only one API request is reaching it at a time: api_lock = asyncio.Lock() + async def async_init_coordinator( + coordinator: GuardianDataUpdateCoordinator, + ) -> None: + """Initialize a GuardianDataUpdateCoordinator.""" + await coordinator.async_initialize() + await coordinator.async_config_entry_first_refresh() + # Set up GuardianDataUpdateCoordinators for the valve controller: valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] @@ -151,13 +158,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api ] = GuardianDataUpdateCoordinator( hass, + entry=entry, client=client, api_name=api, api_coro=api_coro, api_lock=api_lock, valve_controller_uid=entry.data[CONF_UID], ) - init_valve_controller_tasks.append(coordinator.async_refresh()) + init_valve_controller_tasks.append(async_init_coordinator(coordinator)) await asyncio.gather(*init_valve_controller_tasks) @@ -352,6 +360,7 @@ class PairedSensorManager: coordinator = self.coordinators[uid] = GuardianDataUpdateCoordinator( self._hass, + entry=self._entry, client=self._client, api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}", api_coro=lambda: cast( @@ -422,7 +431,7 @@ class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity. + """Update the entity's underlying data. This should be extended by Guardian platforms. """ diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index eb6d49c3ec1..766e5d961e8 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -137,7 +137,7 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: self._attr_is_on = self.coordinator.data["wet"] elif self.entity_description.key == SENSOR_KIND_MOVED: diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index 01efb7deba4..740cce43c62 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -15,6 +15,7 @@ from homeassistant.components.button import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -111,3 +112,5 @@ class GuardianButton(ValveControllerEntity, ButtonEntity): raise HomeAssistantError( f'Error while pressing button "{self.entity_id}": {err}' ) from err + + async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested) diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 5b4c621cce6..05de437b10a 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -128,7 +128,7 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_BATTERY: self._attr_native_value = self.coordinator.data["battery"] elif self.entity_description.key == SENSOR_KIND_TEMPERATURE: @@ -142,7 +142,7 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_TEMPERATURE: self._attr_native_value = self.coordinator.data["temperature"] elif self.entity_description.key == SENSOR_KIND_UPTIME: diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 2cedcf9c1e4..c88d6762e51 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -9,21 +9,28 @@ from typing import Any, cast from aioguardian import Client from aioguardian.errors import GuardianError -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) +SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}" + class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): """Define an extended DataUpdateCoordinator with some Guardian goodies.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, *, + entry: ConfigEntry, client: Client, api_name: str, api_coro: Callable[..., Awaitable], @@ -41,6 +48,12 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): self._api_coro = api_coro self._api_lock = api_lock self._client = client + self._signal_handler_unsubs: list[Callable[..., None]] = [] + + self.config_entry = entry + self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( + self.config_entry.entry_id + ) async def _async_update_data(self) -> dict[str, Any]: """Execute a "locked" API request against the valve controller.""" @@ -50,3 +63,26 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): except GuardianError as err: raise UpdateFailed(err) from err return cast(dict[str, Any], resp["data"]) + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + + @callback + def async_reboot_requested() -> None: + """Respond to a reboot request.""" + self.last_update_success = False + self.async_update_listeners() + + self._signal_handler_unsubs.append( + async_dispatcher_connect( + self.hass, self.signal_reboot_requested, async_reboot_requested + ) + ) + + @callback + def async_teardown() -> None: + """Tear the coordinator down appropriately.""" + for unsub in self._signal_handler_unsubs: + unsub() + + self.config_entry.async_on_unload(async_teardown) From bdb627539ec283b5625e66bb706f372168d4b3ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jul 2022 18:33:29 -1000 Subject: [PATCH 773/820] Fix switchbot failing to setup when last_run_success is not saved (#75887) --- homeassistant/components/switchbot/cover.py | 9 ++++++--- homeassistant/components/switchbot/switch.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index dc9ddf4e616..0ae225f55d7 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -80,9 +80,12 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): if not last_state or ATTR_CURRENT_POSITION not in last_state.attributes: return - self._attr_current_cover_position = last_state.attributes[ATTR_CURRENT_POSITION] - self._last_run_success = last_state.attributes["last_run_success"] - self._attr_is_closed = last_state.attributes[ATTR_CURRENT_POSITION] <= 20 + self._attr_current_cover_position = last_state.attributes.get( + ATTR_CURRENT_POSITION + ) + self._last_run_success = last_state.attributes.get("last_run_success") + if self._attr_current_cover_position is not None: + self._attr_is_closed = self._attr_current_cover_position <= 20 async def async_open_cover(self, **kwargs: Any) -> None: """Open the curtain.""" diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 51f15c488d1..e6ba77fa164 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -69,7 +69,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): if not (last_state := await self.async_get_last_state()): return self._attr_is_on = last_state.state == STATE_ON - self._last_run_success = last_state.attributes["last_run_success"] + self._last_run_success = last_state.attributes.get("last_run_success") async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" From a3276e00b963542eebf4590d7c91c7c1defb52fa Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Sun, 31 Jul 2022 13:28:09 +0200 Subject: [PATCH 774/820] Bump enturclient to 0.2.4 (#75928) --- homeassistant/components/entur_public_transport/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index c7f4fbeef53..3bbacb8c3d4 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "entur_public_transport", "name": "Entur", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", - "requirements": ["enturclient==0.2.3"], + "requirements": ["enturclient==0.2.4"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_polling", "loggers": ["enturclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 58f2e5f73ec..2ad42cf511c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -606,7 +606,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.entur_public_transport -enturclient==0.2.3 +enturclient==0.2.4 # homeassistant.components.environment_canada env_canada==0.5.22 From d84bc20a58233b903bf25afc14ccea88f015b161 Mon Sep 17 00:00:00 2001 From: MasonCrawford Date: Sun, 31 Jul 2022 19:32:40 +0800 Subject: [PATCH 775/820] Small fixes for LG soundbar (#75938) --- homeassistant/components/lg_soundbar/media_player.py | 3 --- homeassistant/components/lg_soundbar/strings.json | 3 +-- homeassistant/components/lg_soundbar/translations/en.json | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index f8f6fcf26fd..941042d5bce 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -47,7 +47,6 @@ class LGDevice(MediaPlayerEntity): self._port = port self._attr_unique_id = unique_id - self._name = None self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -94,8 +93,6 @@ class LGDevice(MediaPlayerEntity): elif response["msg"] == "SPK_LIST_VIEW_INFO": if "i_vol" in data: self._volume = data["i_vol"] - if "s_user_name" in data: - self._name = data["s_user_name"] if "i_vol_min" in data: self._volume_min = data["i_vol_min"] if "i_vol_max" in data: diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json index ef7bf32a051..52d57eda809 100644 --- a/homeassistant/components/lg_soundbar/strings.json +++ b/homeassistant/components/lg_soundbar/strings.json @@ -11,8 +11,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "existing_instance_updated": "Updated existing configuration.", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json index a646279203f..10441d21536 100644 --- a/homeassistant/components/lg_soundbar/translations/en.json +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is already configured", - "existing_instance_updated": "Updated existing configuration." + "already_configured": "Device is already configured" }, "error": { "cannot_connect": "Failed to connect" From 38ae2f4e9e4fdcf02d10a6ab4a12289a77b48af4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Jul 2022 06:15:06 -0700 Subject: [PATCH 776/820] Bump govee-ble to fix H5179 sensors (#75957) Changelog: https://github.com/Bluetooth-Devices/govee-ble/compare/v0.12.4...v0.12.5 --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 270858d04d4..c7909d3e1af 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -24,7 +24,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.4"], + "requirements": ["govee-ble==0.12.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 2ad42cf511c..0ce0cb1529b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.4 +govee-ble==0.12.5 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7358e148dc..e619216cc0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.4 +govee-ble==0.12.5 # homeassistant.components.gree greeclimate==1.2.0 From d205fb5064f01681a768afa4c1e186accc091660 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 31 Jul 2022 12:21:25 +0200 Subject: [PATCH 777/820] Handle failed connection attempts in opentherm_gw (#75961) --- .../components/opentherm_gw/__init__.py | 18 ++++++++++++++++-- .../components/opentherm_gw/config_flow.py | 15 +++++++++++---- homeassistant/components/opentherm_gw/const.py | 2 ++ .../components/opentherm_gw/strings.json | 3 ++- .../opentherm_gw/test_config_flow.py | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 7c27eeceede..cdf360c8795 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,9 +1,11 @@ """Support for OpenTherm Gateway devices.""" +import asyncio from datetime import date, datetime import logging import pyotgw import pyotgw.vars as gw_vars +from serial import SerialException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -23,6 +25,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -37,6 +40,7 @@ from .const import ( CONF_PRECISION, CONF_READ_PRECISION, CONF_SET_PRECISION, + CONNECTION_TIMEOUT, DATA_GATEWAYS, DATA_OPENTHERM_GW, DOMAIN, @@ -107,8 +111,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.add_update_listener(options_updated) - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe()) + try: + await asyncio.wait_for( + gateway.connect_and_subscribe(), + timeout=CONNECTION_TIMEOUT, + ) + except (asyncio.TimeoutError, ConnectionError, SerialException) as ex: + raise ConfigEntryNotReady( + f"Could not connect to gateway at {gateway.device_path}: {ex}" + ) from ex await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -428,6 +439,9 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" self.status = await self.gateway.connect(self.device_path) + if not self.status: + await self.cleanup() + raise ConnectionError version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) self.gw_version = version_string[18:] if version_string else None _LOGGER.debug( diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 3f91496adab..c3a955b2387 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -26,6 +26,7 @@ from .const import ( CONF_READ_PRECISION, CONF_SET_PRECISION, CONF_TEMPORARY_OVRD_MODE, + CONNECTION_TIMEOUT, ) @@ -62,15 +63,21 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): otgw = pyotgw.OpenThermGateway() status = await otgw.connect(device) await otgw.disconnect() + if not status: + raise ConnectionError return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) try: - res = await asyncio.wait_for(test_connection(), timeout=10) - except (asyncio.TimeoutError, SerialException): + await asyncio.wait_for( + test_connection(), + timeout=CONNECTION_TIMEOUT, + ) + except asyncio.TimeoutError: + return self._show_form({"base": "timeout_connect"}) + except (ConnectionError, SerialException): return self._show_form({"base": "cannot_connect"}) - if res: - return self._create_entry(gw_id, name, device) + return self._create_entry(gw_id, name, device) return self._show_form() diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index a5042628529..d72469759f1 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -25,6 +25,8 @@ CONF_READ_PRECISION = "read_precision" CONF_SET_PRECISION = "set_precision" CONF_TEMPORARY_OVRD_MODE = "temporary_override_mode" +CONNECTION_TIMEOUT = 10 + DATA_GATEWAYS = "gateways" DATA_OPENTHERM_GW = "opentherm_gw" diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index f53ffeda6f6..a80a059481d 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -12,7 +12,8 @@ "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "id_exists": "Gateway id already exists", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" } }, "options": { diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 46d53bc54b5..080e9a96d58 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -164,7 +164,7 @@ async def test_form_connection_timeout(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": "timeout_connect"} assert len(mock_connect.mock_calls) == 1 From 58265664d1848f4e78cc745cc9e59d9e6b563c44 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jul 2022 18:00:42 +0200 Subject: [PATCH 778/820] Improve authentication handling for camera view (#75979) --- homeassistant/components/camera/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 3bf86dedea1..77bd0b57f1c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -14,7 +14,7 @@ import os from random import SystemRandom from typing import Final, Optional, cast, final -from aiohttp import web +from aiohttp import hdrs, web import async_timeout import attr import voluptuous as vol @@ -715,8 +715,11 @@ class CameraView(HomeAssistantView): ) if not authenticated: - if request[KEY_AUTHENTICATED]: + # Attempt with invalid bearer token, raise unauthorized + # so ban middleware can handle it. + if hdrs.AUTHORIZATION in request.headers: raise web.HTTPUnauthorized() + # Invalid sigAuth or camera access token raise web.HTTPForbidden() if not camera.is_on: From 26a3621bb3eda49cebffbb7e82fa5e952c1affbe Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 31 Jul 2022 21:14:03 +0200 Subject: [PATCH 779/820] Bump pyotgw to 2.0.2 (#75980) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 0bc69387d0b..02b1604ea11 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.1"], + "requirements": ["pyotgw==2.0.2"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 0ce0cb1529b..75f6000bd3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1725,7 +1725,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e619216cc0b..495c91e52ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1190,7 +1190,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From e330147751d6305348fd4939692f52931f4dacb1 Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 31 Jul 2022 16:14:30 -0400 Subject: [PATCH 780/820] Bump AIOAladdinConnect to 0.1.33 (#75986) Bump aladdin_connect 0.1.33 --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index d551b91bce9..a142e838f3e 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.31"], + "requirements": ["AIOAladdinConnect==0.1.33"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 75f6000bd3c..fc5b29770d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.31 +AIOAladdinConnect==0.1.33 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 495c91e52ab..06b8d92a551 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.31 +AIOAladdinConnect==0.1.33 # homeassistant.components.adax Adax-local==0.1.4 From ebf91fe46b143a2abbfdca7c392deb85c26f56ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jul 2022 13:13:05 -0700 Subject: [PATCH 781/820] Bump pySwitchbot to 0.16.0 to fix compat with bleak 0.15 (#75991) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index d6acb69431e..dcb33c03882 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.15.2"], + "requirements": ["PySwitchbot==0.16.0"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], diff --git a/requirements_all.txt b/requirements_all.txt index fc5b29770d2..aeaf75b987e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.2 +PySwitchbot==0.16.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06b8d92a551..03cf9488f37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.2 +PySwitchbot==0.16.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From ffd2813150c9375c6136ff04aac7bdf5776fe3c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jul 2022 22:14:09 +0200 Subject: [PATCH 782/820] Fix Home Connect services not being set up (#75997) --- .../components/home_connect/__init__.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index a2876dd86f5..6e664ad07e4 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -117,24 +117,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Home Connect component.""" hass.data[DOMAIN] = {} - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of Home Connect integration in YAML is deprecated and " - "will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) + if DOMAIN in config: + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential( + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + ), + ) + _LOGGER.warning( + "Configuration of Home Connect integration in YAML is deprecated and " + "will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) async def _async_service_program(call, method): """Execute calls to services taking a program.""" From 5ab549653ba9ddc9a3b18000dbc772e4f154b624 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Jul 2022 13:29:52 -0700 Subject: [PATCH 783/820] Bumped version to 2022.8.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4a37902e79f..9ef45865665 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 8fca49a67f2..284585734dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b3" +version = "2022.8.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2a58bf06c1f972155f4ab261145946e58bcf7340 Mon Sep 17 00:00:00 2001 From: rhadamantys <46837767+rhadamantys@users.noreply.github.com> Date: Mon, 1 Aug 2022 17:42:47 +0200 Subject: [PATCH 784/820] Fix invalid enocean unique_id (#74508) Co-authored-by: Martin Hjelmare --- homeassistant/components/enocean/switch.py | 43 +++++++++++-- tests/components/enocean/test_switch.py | 73 ++++++++++++++++++++++ 2 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 tests/components/enocean/test_switch.py diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index a53f691df19..5edd2bb6155 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -5,12 +5,14 @@ from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_ID, CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import DOMAIN, LOGGER from .device import EnOceanEntity CONF_CHANNEL = "channel" @@ -25,10 +27,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +def generate_unique_id(dev_id: list[int], channel: int) -> str: + """Generate a valid unique id.""" + return f"{combine_hex(dev_id)}-{channel}" + + +def _migrate_to_new_unique_id(hass: HomeAssistant, dev_id, channel) -> None: + """Migrate old unique ids to new unique ids.""" + old_unique_id = f"{combine_hex(dev_id)}" + + ent_reg = entity_registry.async_get(hass) + entity_id = ent_reg.async_get_entity_id(Platform.SWITCH, DOMAIN, old_unique_id) + + if entity_id is not None: + new_unique_id = generate_unique_id(dev_id, channel) + try: + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + except ValueError: + LOGGER.warning( + "Skip migration of id [%s] to [%s] because it already exists", + old_unique_id, + new_unique_id, + ) + else: + LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) + + +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the EnOcean switch platform.""" @@ -36,7 +68,8 @@ def setup_platform( dev_id = config.get(CONF_ID) dev_name = config.get(CONF_NAME) - add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) + _migrate_to_new_unique_id(hass, dev_id, channel) + async_add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) class EnOceanSwitch(EnOceanEntity, SwitchEntity): @@ -49,7 +82,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): self._on_state = False self._on_state2 = False self.channel = channel - self._attr_unique_id = f"{combine_hex(dev_id)}" + self._attr_unique_id = generate_unique_id(dev_id, channel) @property def is_on(self): diff --git a/tests/components/enocean/test_switch.py b/tests/components/enocean/test_switch.py new file mode 100644 index 00000000000..a7aafa6fc73 --- /dev/null +++ b/tests/components/enocean/test_switch.py @@ -0,0 +1,73 @@ +"""Tests for the EnOcean switch platform.""" + +from enocean.utils import combine_hex + +from homeassistant.components.enocean import DOMAIN as ENOCEAN_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, assert_setup_component + +SWITCH_CONFIG = { + "switch": [ + { + "platform": ENOCEAN_DOMAIN, + "id": [0xDE, 0xAD, 0xBE, 0xEF], + "channel": 1, + "name": "room0", + }, + ] +} + + +async def test_unique_id_migration(hass: HomeAssistant) -> None: + """Test EnOcean switch ID migration.""" + + entity_name = SWITCH_CONFIG["switch"][0]["name"] + switch_entity_id = f"{SWITCH_DOMAIN}.{entity_name}" + dev_id = SWITCH_CONFIG["switch"][0]["id"] + channel = SWITCH_CONFIG["switch"][0]["channel"] + + ent_reg = er.async_get(hass) + + old_unique_id = f"{combine_hex(dev_id)}" + + entry = MockConfigEntry(domain=ENOCEAN_DOMAIN, data={"device": "/dev/null"}) + + entry.add_to_hass(hass) + + # Add a switch with an old unique_id to the entity registry + entity_entry = ent_reg.async_get_or_create( + SWITCH_DOMAIN, + ENOCEAN_DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=entry, + original_name=entity_name, + ) + + assert entity_entry.entity_id == switch_entity_id + assert entity_entry.unique_id == old_unique_id + + # Now add the sensor to check, whether the old unique_id is migrated + + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + SWITCH_CONFIG, + ) + + await hass.async_block_till_done() + + # Check that new entry has a new unique_id + entity_entry = ent_reg.async_get(switch_entity_id) + new_unique_id = f"{combine_hex(dev_id)}-{channel}" + + assert entity_entry.unique_id == new_unique_id + assert ( + ent_reg.async_get_entity_id(SWITCH_DOMAIN, ENOCEAN_DOMAIN, old_unique_id) + is None + ) From 990975e9083df99f29a686cb1215af997be7129a Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 1 Aug 2022 16:56:08 +0200 Subject: [PATCH 785/820] =?UTF-8?q?Convert=20fj=C3=A4r=C3=A5skupan=20to=20?= =?UTF-8?q?built=20in=20bluetooth=20(#75380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add bluetooth discovery * Use home assistant standard api * Fixup manufacture data * Adjust config flow to use standard features * Fixup tests * Mock bluetooth * Simplify device check * Fix missing typing Co-authored-by: Martin Hjelmare --- .../components/fjaraskupan/__init__.py | 87 +++++++++---------- .../components/fjaraskupan/config_flow.py | 29 ++----- .../components/fjaraskupan/manifest.json | 9 +- homeassistant/generated/bluetooth.py | 12 +++ tests/components/fjaraskupan/__init__.py | 10 +++ tests/components/fjaraskupan/conftest.py | 46 +--------- .../fjaraskupan/test_config_flow.py | 48 +++++----- 7 files changed, 109 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 36608fb026d..fbd2f13d2b4 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -5,14 +5,20 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging -from typing import TYPE_CHECKING -from bleak import BleakScanner -from fjaraskupan import Device, State, device_filter +from fjaraskupan import Device, State +from homeassistant.components.bluetooth import ( + BluetoothCallbackMatcher, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_address_present, + async_register_callback, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -23,11 +29,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DISPATCH_DETECTION, DOMAIN -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - from bleak.backends.scanner import AdvertisementData - - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.FAN, @@ -70,16 +71,18 @@ class Coordinator(DataUpdateCoordinator[State]): async def _async_update_data(self) -> State: """Handle an explicit update request.""" if self._refresh_was_scheduled: - raise UpdateFailed("No data received within schedule.") + if async_address_present(self.hass, self.device.address): + return self.device.state + raise UpdateFailed( + "No data received within schedule, and device is no longer present" + ) await self.device.update() return self.device.state - def detection_callback( - self, ble_device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: + def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None: """Handle a new announcement of data.""" - self.device.detection_callback(ble_device, advertisement_data) + self.device.detection_callback(service_info.device, service_info.advertisement) self.async_set_updated_data(self.device.state) @@ -87,59 +90,52 @@ class Coordinator(DataUpdateCoordinator[State]): class EntryState: """Store state of config entry.""" - scanner: BleakScanner coordinators: dict[str, Coordinator] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"DuplicateData": True}) - - state = EntryState(scanner, {}) + state = EntryState({}) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = state - async def detection_callback( - ble_device: BLEDevice, advertisement_data: AdvertisementData + def detection_callback( + service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: - if data := state.coordinators.get(ble_device.address): - _LOGGER.debug( - "Update: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - data.detection_callback(ble_device, advertisement_data) + if change != BluetoothChange.ADVERTISEMENT: + return + if data := state.coordinators.get(service_info.address): + _LOGGER.debug("Update: %s", service_info) + data.detection_callback(service_info) else: - if not device_filter(ble_device, advertisement_data): - return + _LOGGER.debug("Detected: %s", service_info) - _LOGGER.debug( - "Detected: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - device = Device(ble_device) + device = Device(service_info.device) device_info = DeviceInfo( - identifiers={(DOMAIN, ble_device.address)}, + identifiers={(DOMAIN, service_info.address)}, manufacturer="Fjäråskupan", name="Fjäråskupan", ) coordinator: Coordinator = Coordinator(hass, device, device_info) - coordinator.detection_callback(ble_device, advertisement_data) + coordinator.detection_callback(service_info) - state.coordinators[ble_device.address] = coordinator + state.coordinators[service_info.address] = coordinator async_dispatcher_send( hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator ) - scanner.register_detection_callback(detection_callback) - await scanner.start() - - async def on_hass_stop(event: Event) -> None: - await scanner.stop() - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + async_register_callback( + hass, + detection_callback, + BluetoothCallbackMatcher( + manufacturer_id=20296, + manufacturer_data_start=[79, 68, 70, 74, 65, 82], + ), + BluetoothScanningMode.ACTIVE, + ) ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -177,7 +173,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - entry_state: EntryState = hass.data[DOMAIN].pop(entry.entry_id) - await entry_state.scanner.stop() + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index ffac366500b..dd1dc03d3ad 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -1,42 +1,25 @@ """Config flow for Fjäråskupan integration.""" from __future__ import annotations -import asyncio - -import async_timeout -from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from fjaraskupan import device_filter +from homeassistant.components.bluetooth import async_discovered_service_info from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow from .const import DOMAIN -CONST_WAIT_TIME = 5.0 - async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" - event = asyncio.Event() + service_infos = async_discovered_service_info(hass) - def detection(device: BLEDevice, advertisement_data: AdvertisementData): - if device_filter(device, advertisement_data): - event.set() + for service_info in service_infos: + if device_filter(service_info.device, service_info.advertisement): + return True - async with BleakScanner( - detection_callback=detection, - filters={"DuplicateData": True}, - ): - try: - async with async_timeout.timeout(CONST_WAIT_TIME): - await event.wait() - except asyncio.TimeoutError: - return False - - return True + return False register_discovery_flow(DOMAIN, "Fjäråskupan", _async_has_devices) diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index 3ff6e599a6b..bf7956d297d 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -6,5 +6,12 @@ "requirements": ["fjaraskupan==1.0.2"], "codeowners": ["@elupus"], "iot_class": "local_polling", - "loggers": ["bleak", "fjaraskupan"] + "loggers": ["bleak", "fjaraskupan"], + "dependencies": ["bluetooth"], + "bluetooth": [ + { + "manufacturer_id": 20296, + "manufacturer_data_start": [79, 68, 70, 74, 65, 82] + } + ] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 2cbaebb6074..ef8193dad28 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,18 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, str | int | list[int]]] = [ + { + "domain": "fjaraskupan", + "manufacturer_id": 20296, + "manufacturer_data_start": [ + 79, + 68, + 70, + 74, + 65, + 82 + ] + }, { "domain": "govee_ble", "local_name": "Govee*" diff --git a/tests/components/fjaraskupan/__init__.py b/tests/components/fjaraskupan/__init__.py index 26a5ecd6605..35c69f98d65 100644 --- a/tests/components/fjaraskupan/__init__.py +++ b/tests/components/fjaraskupan/__init__.py @@ -1 +1,11 @@ """Tests for the Fjäråskupan integration.""" + + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import SOURCE_LOCAL, BluetoothServiceInfoBleak + +COOKER_SERVICE_INFO = BluetoothServiceInfoBleak.from_advertisement( + BLEDevice("1.1.1.1", "COOKERHOOD_FJAR"), AdvertisementData(), source=SOURCE_LOCAL +) diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index 4e06b2ad046..46ff5ae167a 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -1,47 +1,9 @@ """Standard fixtures for the Fjäråskupan integration.""" from __future__ import annotations -from unittest.mock import patch - -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData, BaseBleakScanner -from pytest import fixture +import pytest -@fixture(name="scanner", autouse=True) -def fixture_scanner(hass): - """Fixture for scanner.""" - - devices = [BLEDevice("1.1.1.1", "COOKERHOOD_FJAR")] - - class MockScanner(BaseBleakScanner): - """Mock Scanner.""" - - def __init__(self, *args, **kwargs) -> None: - """Initialize the scanner.""" - super().__init__( - detection_callback=kwargs.pop("detection_callback"), service_uuids=[] - ) - - async def start(self): - """Start scanning for devices.""" - for device in devices: - self._callback(device, AdvertisementData()) - - async def stop(self): - """Stop scanning for devices.""" - - @property - def discovered_devices(self) -> list[BLEDevice]: - """Return discovered devices.""" - return devices - - def set_scanning_filter(self, **kwargs): - """Set the scanning filter.""" - - with patch( - "homeassistant.components.fjaraskupan.config_flow.BleakScanner", new=MockScanner - ), patch( - "homeassistant.components.fjaraskupan.config_flow.CONST_WAIT_TIME", new=0.01 - ): - yield devices +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index a51b8c6f9fa..bef53e18073 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations from unittest.mock import patch -from bleak.backends.device import BLEDevice from pytest import fixture from homeassistant import config_entries @@ -11,6 +10,8 @@ from homeassistant.components.fjaraskupan.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import COOKER_SERVICE_INFO + @fixture(name="mock_setup_entry", autouse=True) async def fixture_mock_setup_entry(hass): @@ -24,31 +25,38 @@ async def fixture_mock_setup_entry(hass): async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[COOKER_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Fjäråskupan" - assert result["data"] == {} + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Fjäråskupan" + assert result["data"] == {} - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 -async def test_scan_no_devices(hass: HomeAssistant, scanner: list[BLEDevice]) -> None: +async def test_scan_no_devices(hass: HomeAssistant) -> None: """Test we get the form.""" - scanner.clear() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "no_devices_found" + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" From c6038380d68f1745759070d1b98aca14a2716eb6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Aug 2022 18:20:20 -0700 Subject: [PATCH 786/820] Add repair issues for nest app auth removal and yaml deprecation (#75974) * Add repair issues for nest app auth removal and yaml deprecation * Apply PR feedback * Re-apply suggestion that i force pushed over * Update criticality level --- homeassistant/components/nest/__init__.py | 33 +++++++++++++++---- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/strings.json | 10 ++++++ .../components/nest/translations/en.json | 18 +++++----- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 72759ac0f52..44658558b62 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -29,6 +29,11 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.camera import Image, img_util from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.repairs import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, @@ -187,6 +192,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, unique_id=entry.data[CONF_PROJECT_ID] ) + async_delete_issue(hass, DOMAIN, "removed_app_auth") + subscriber = await api.new_subscriber(hass, entry) if not subscriber: return False @@ -255,6 +262,18 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN: # App Auth credentials have been deprecated and must be re-created # by the user in the config flow + async_create_issue( + hass, + DOMAIN, + "removed_app_auth", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="removed_app_auth", + translation_placeholders={ + "more_info_url": "https://www.home-assistant.io/more-info/nest-auth-deprecation", + "documentation_url": "https://www.home-assistant.io/integrations/nest/", + }, + ) raise ConfigEntryAuthFailed( "Google has deprecated App Auth credentials, and the integration " "must be reconfigured in the UI to restore access to Nest Devices." @@ -271,12 +290,14 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: WEB_AUTH_DOMAIN, ) - _LOGGER.warning( - "Configuration of Nest integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " - "(including OAuth Application Credentials) has been imported into " - "the UI automatically and can be safely removed from your " - "configuration.yaml file" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", ) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 72e0aed8420..d826272b207 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "application_credentials"], + "dependencies": ["ffmpeg", "http", "application_credentials", "repairs"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"], diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 0a13de41511..07ba63ac479 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -88,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Nest YAML configuration is being removed", + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_app_auth": { + "title": "Nest Authentication Credentials must be updated", + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information." + } } } diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 5f026e55f31..cd8274d635a 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -10,7 +10,6 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", - "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)" }, "step": { - "auth": { - "data": { - "code": "Access Token" - }, - "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", - "title": "Link Google Account" - }, "auth_upgrade": { "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", "title": "Nest: App Auth Deprecation" @@ -96,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Nest YAML configuration is being removed" + }, + "removed_app_auth": { + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information.", + "title": "Nest Authentication Credentials must be updated" + } } } \ No newline at end of file From 75747ce319532c68a4897875a8b2ebace9b8a98b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 1 Aug 2022 14:22:24 +0200 Subject: [PATCH 787/820] Support MWh for gas consumption sensors (#76016) --- homeassistant/components/energy/validate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 02549ddfe96..9d6b3bd53c7 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -40,7 +40,11 @@ GAS_USAGE_DEVICE_CLASSES = ( sensor.SensorDeviceClass.GAS, ) GAS_USAGE_UNITS = { - sensor.SensorDeviceClass.ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), + sensor.SensorDeviceClass.ENERGY: ( + ENERGY_WATT_HOUR, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ), sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET), } GAS_PRICE_UNITS = tuple( From a332eb154c3fc6a25364a4f88a2bbdedc302565c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 1 Aug 2022 22:04:16 +0100 Subject: [PATCH 788/820] Add reauth flow to xiaomi_ble, fixes problem adding LYWSD03MMC (#76028) --- .../components/xiaomi_ble/config_flow.py | 136 +++++--- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 45 ++- .../components/xiaomi_ble/strings.json | 4 + .../xiaomi_ble/translations/en.json | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/xiaomi_ble/test_config_flow.py | 293 +++++++++++++++++- 8 files changed, 439 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index aa1ffc24895..092c60e9713 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import dataclasses from typing import Any @@ -12,7 +13,7 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothScanningMode, - BluetoothServiceInfoBleak, + BluetoothServiceInfo, async_discovered_service_info, async_process_advertisements, ) @@ -31,11 +32,11 @@ class Discovery: """A discovered bluetooth device.""" title: str - discovery_info: BluetoothServiceInfoBleak + discovery_info: BluetoothServiceInfo device: DeviceData -def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str: +def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name @@ -46,19 +47,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovery_info: BluetoothServiceInfo | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} async def _async_wait_for_full_advertisement( - self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData - ) -> BluetoothServiceInfoBleak: + self, discovery_info: BluetoothServiceInfo, device: DeviceData + ) -> BluetoothServiceInfo: """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" if not device.pending: return discovery_info def _process_more_advertisements( - service_info: BluetoothServiceInfoBleak, + service_info: BluetoothServiceInfo, ) -> bool: device.update(service_info) return not device.pending @@ -72,7 +73,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfoBleak + self, discovery_info: BluetoothServiceInfo ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) @@ -81,20 +82,21 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if not device.supported(discovery_info): return self.async_abort(reason="not_supported") + title = _title(discovery_info, device) + self.context["title_placeholders"] = {"name": title} + + self._discovered_device = device + # Wait until we have received enough information about this device to detect its encryption type try: - discovery_info = await self._async_wait_for_full_advertisement( + self._discovery_info = await self._async_wait_for_full_advertisement( discovery_info, device ) except asyncio.TimeoutError: - # If we don't see a valid packet within the timeout then this device is not supported. - return self.async_abort(reason="not_supported") - - self._discovery_info = discovery_info - self._discovered_device = device - - title = _title(discovery_info, device) - self.context["title_placeholders"] = {"name": title} + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: return await self.async_step_get_encryption_key_legacy() @@ -107,6 +109,8 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Enter a legacy bindkey for a v2/v3 MiBeacon device.""" assert self._discovery_info + assert self._discovered_device + errors = {} if user_input is not None: @@ -115,18 +119,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if len(bindkey) != 24: errors["bindkey"] = "expected_24_characters" else: - device = DeviceData(bindkey=bytes.fromhex(bindkey)) + self._discovered_device.bindkey = bytes.fromhex(bindkey) # If we got this far we already know supported will # return true so we don't bother checking that again # We just want to retry the decryption - device.supported(self._discovery_info) + self._discovered_device.supported(self._discovery_info) - if device.bindkey_verified: - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={"bindkey": bindkey}, - ) + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) errors["bindkey"] = "decryption_failed" @@ -142,6 +143,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Enter a bindkey for a v4/v5 MiBeacon device.""" assert self._discovery_info + assert self._discovered_device errors = {} @@ -151,18 +153,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if len(bindkey) != 32: errors["bindkey"] = "expected_32_characters" else: - device = DeviceData(bindkey=bytes.fromhex(bindkey)) + self._discovered_device.bindkey = bytes.fromhex(bindkey) # If we got this far we already know supported will # return true so we don't bother checking that again # We just want to retry the decryption - device.supported(self._discovery_info) + self._discovered_device.supported(self._discovery_info) - if device.bindkey_verified: - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={"bindkey": bindkey}, - ) + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) errors["bindkey"] = "decryption_failed" @@ -178,10 +177,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={}, - ) + return self._async_get_or_create_entry() self._set_confirm_only() return self.async_show_form( @@ -189,6 +185,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders=self.context["title_placeholders"], ) + async def async_step_confirm_slow( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ack that device is slow.""" + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + return self._async_get_or_create_entry() + + self._set_confirm_only() + return self.async_show_form( + step_id="confirm_slow", + description_placeholders=self.context["title_placeholders"], + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -198,24 +207,28 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) discovery = self._discovered_devices[address] + self.context["title_placeholders"] = {"name": discovery.title} + # Wait until we have received enough information about this device to detect its encryption type try: self._discovery_info = await self._async_wait_for_full_advertisement( discovery.discovery_info, discovery.device ) except asyncio.TimeoutError: - # If we don't see a valid packet within the timeout then this device is not supported. - return self.async_abort(reason="not_supported") + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() + + self._discovered_device = discovery.device if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: - self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_legacy() if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: - self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_4_5() - return self.async_create_entry(title=discovery.title, data={}) + return self._async_get_or_create_entry() current_addresses = self._async_current_ids() for discovery_info in async_discovered_service_info(self.hass): @@ -241,3 +254,46 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a flow initialized by a reauth event.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry is not None + + device: DeviceData = self.context["device"] + self._discovered_device = device + + self._discovery_info = device.last_service_info + + if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + return await self.async_step_get_encryption_key_legacy() + + if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + return await self.async_step_get_encryption_key_4_5() + + # Otherwise there wasn't actually encryption so abort + return self.async_abort(reason="reauth_successful") + + def _async_get_or_create_entry(self, bindkey=None): + data = {} + + if bindkey: + data["bindkey"] = bindkey + + if entry_id := self.context.get("entry_id"): + entry = self.hass.config_entries.async_get_entry(entry_id) + assert entry is not None + + self.hass.config_entries.async_update_entry(entry, data=data) + + # Reload the config entry to notify of updated config + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data=data, + ) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 0d97dcbedf8..a901439b2c9 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.2"], + "requirements": ["xiaomi-ble==0.6.4"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index a96722620a9..b3cd5126967 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -11,8 +11,10 @@ from xiaomi_ble import ( Units, XiaomiBluetoothDeviceData, ) +from xiaomi_ble.parser import EncryptionScheme from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, @@ -163,6 +165,45 @@ def sensor_update_to_bluetooth_data_update( ) +def process_service_info( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + data: XiaomiBluetoothDeviceData, + service_info: BluetoothServiceInfoBleak, +) -> PassiveBluetoothDataUpdate: + """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + update = data.update(service_info) + + # If device isn't pending we know it has seen at least one broadcast with a payload + # If that payload was encrypted and the bindkey was not verified then we need to reauth + if ( + not data.pending + and data.encryption_scheme != EncryptionScheme.NONE + and not data.bindkey_verified + ): + flow_context = { + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + "device": data, + } + + for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): + if flow["context"] == flow_context: + break + else: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context=flow_context, + data=entry.data, + ) + ) + + return sensor_update_to_bluetooth_data_update(update) + + async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, @@ -177,9 +218,7 @@ async def async_setup_entry( kwargs["bindkey"] = bytes.fromhex(bindkey) data = XiaomiBluetoothDeviceData(**kwargs) processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) + lambda service_info: process_service_info(hass, entry, data, service_info) ) entry.async_on_unload( processor.async_add_entities_listener( diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index e12a15a0671..48d5c3a87f7 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -11,6 +11,9 @@ "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + }, "get_encryption_key_legacy": { "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", "data": { @@ -25,6 +28,7 @@ } }, "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index b9f4e024f92..836ccc51637 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -6,7 +6,8 @@ "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", - "no_devices_found": "No devices found on the network" + "no_devices_found": "No devices found on the network", + "reauth_successful": "Re-authentication was successful" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + }, "user": { "data": { "address": "Device" diff --git a/requirements_all.txt b/requirements_all.txt index aeaf75b987e..4d3e28cce46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2470,7 +2470,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.2 +xiaomi-ble==0.6.4 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03cf9488f37..431483efd32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.2 +xiaomi-ble==0.6.4 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 86fda21aaa0..0d123f0cd54 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -3,7 +3,10 @@ import asyncio from unittest.mock import patch +from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData + from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.data_entry_flow import FlowResultType @@ -52,8 +55,19 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): context={"source": config_entries.SOURCE_BLUETOOTH}, data=MISSING_PAYLOAD_ENCRYPTED, ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "not_supported" + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): @@ -318,6 +332,24 @@ async def test_async_step_user_no_devices_found(hass): assert result["reason"] == "no_devices_found" +async def test_async_step_user_no_devices_found_2(hass): + """ + Test setup from service info cache with no devices found. + + This variant tests with a non-Xiaomi device known to us. + """ + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[NOT_SENSOR_PUSH_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + async def test_async_step_user_with_found_devices(hass): """Test setup from service info cache with devices found.""" with patch( @@ -363,8 +395,19 @@ async def test_async_step_user_short_payload(hass): result["flow_id"], user_input={"address": "A4:C1:38:56:53:84"}, ) - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "not_supported" + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "LYWSD02MMC" + assert result3["data"] == {} + assert result3["result"].unique_id == "A4:C1:38:56:53:84" async def test_async_step_user_short_payload_then_full(hass): @@ -755,3 +798,245 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): # Verify the original one was aborted assert not hass.config_entries.flow.async_progress(DOMAIN) + + +async def test_async_step_reauth_legacy(hass): + """Test reauth with a legacy key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_legacy_wrong_key(hass): + """Test reauth with a bad legacy key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b85307515a487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4(hass): + """Test reauth with a v4 key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4_wrong_key(hass): + """Test reauth for v4 with a bad key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_abort_early(hass): + """ + Test we can abort the reauth if there is no encryption. + + (This can't currently happen in practice). + """ + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + + device = DeviceData() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + "device": device, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" From b962a6e7677aa36c443db1004604355f896a7803 Mon Sep 17 00:00:00 2001 From: krazos Date: Mon, 1 Aug 2022 12:45:18 -0400 Subject: [PATCH 789/820] Fix capitalization of Sonos "Status light" entity name (#76035) Tweak capitalization of "Status light" entity name Tweak capitalization of "Status light" entity name for consistency with blog post guidance, which states that entity names should start with a capital letter, with the rest of the words lower case --- homeassistant/components/sonos/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 8dcf47fd0a2..a348b40cb0f 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -72,7 +72,7 @@ FRIENDLY_NAMES = { ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround music full volume", ATTR_NIGHT_SOUND: "Night sound", ATTR_SPEECH_ENHANCEMENT: "Speech enhancement", - ATTR_STATUS_LIGHT: "Status Light", + ATTR_STATUS_LIGHT: "Status light", ATTR_SUB_ENABLED: "Subwoofer enabled", ATTR_SURROUND_ENABLED: "Surround enabled", ATTR_TOUCH_CONTROLS: "Touch controls", From 6b588d41ffc9a6ecab655cde672d7a91c8ce7a20 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 1 Aug 2022 11:43:07 -0400 Subject: [PATCH 790/820] Enhance logging for ZHA device trigger validation (#76036) * Enhance logging for ZHA device trigger validation * use IntegrationError --- homeassistant/components/zha/core/gateway.py | 4 +++- homeassistant/components/zha/core/helpers.py | 18 ++++++++++++++++-- homeassistant/components/zha/device_trigger.py | 4 ++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index f2fd226249b..14fbf2cf701 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -142,6 +142,7 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + self.initialized: bool = False async def async_initialize(self) -> None: """Initialize controller and connect radio.""" @@ -183,6 +184,7 @@ class ZHAGateway: self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) self.async_load_devices() self.async_load_groups() + self.initialized = True @callback def async_load_devices(self) -> None: @@ -217,7 +219,7 @@ class ZHAGateway: async def async_initialize_devices_and_entities(self) -> None: """Initialize devices and load entities.""" - _LOGGER.debug("Loading all devices") + _LOGGER.debug("Initializing all devices from Zigpy cache") await asyncio.gather( *(dev.async_initialize(from_cache=True) for dev in self.devices.values()) ) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b60f61b1e8e..7fd789ac3f5 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -26,6 +26,7 @@ import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback +from homeassistant.exceptions import IntegrationError from homeassistant.helpers import device_registry as dr from .const import ( @@ -42,6 +43,7 @@ if TYPE_CHECKING: from .gateway import ZHAGateway _T = TypeVar("_T") +_LOGGER = logging.getLogger(__name__) @dataclass @@ -170,10 +172,22 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) if not registry_device: + _LOGGER.error("Device id `%s` not found in registry", device_id) raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = zigpy.types.EUI64.convert(ieee_address) + if not zha_gateway.initialized: + _LOGGER.error("Attempting to get a ZHA device when ZHA is not initialized") + raise IntegrationError("ZHA is not initialized yet") + try: + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = zigpy.types.EUI64.convert(ieee_address) + except (IndexError, ValueError) as ex: + _LOGGER.error( + "Unable to determine device IEEE for device with device id `%s`", device_id + ) + raise KeyError( + f"Unable to determine device IEEE for device with device id `{device_id}`." + ) from ex return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 44682aaa559..cdd98110f83 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -13,7 +13,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, IntegrationError from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -39,7 +39,7 @@ async def async_validate_trigger_config( trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError) as err: + except (KeyError, AttributeError, IntegrationError) as err: raise InvalidDeviceAutomationConfig from err if ( zha_device.device_automation_triggers is None From 4f671bccbcca0175d377b785e7594eded83fb9a3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 08:54:28 +0200 Subject: [PATCH 791/820] Support multiple trigger instances for a single webhook (#76037) --- homeassistant/components/webhook/trigger.py | 65 +++++++++++++++------ tests/components/mobile_app/test_webhook.py | 2 +- tests/components/webhook/test_trigger.py | 63 ++++++++++++++++++-- 3 files changed, 107 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 3f790b1ec42..498a7363a61 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -1,5 +1,7 @@ """Offer webhook triggered automation rules.""" -from functools import partial +from __future__ import annotations + +from dataclasses import dataclass from aiohttp import hdrs import voluptuous as vol @@ -13,7 +15,7 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import async_register, async_unregister +from . import DOMAIN, async_register, async_unregister # mypy: allow-untyped-defs @@ -26,20 +28,35 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( } ) +WEBHOOK_TRIGGERS = f"{DOMAIN}_triggers" -async def _handle_webhook(job, trigger_data, hass, webhook_id, request): + +@dataclass +class TriggerInstance: + """Attached trigger settings.""" + + automation_info: AutomationTriggerInfo + job: HassJob + + +async def _handle_webhook(hass, webhook_id, request): """Handle incoming webhook.""" - result = {"platform": "webhook", "webhook_id": webhook_id} + base_result = {"platform": "webhook", "webhook_id": webhook_id} if "json" in request.headers.get(hdrs.CONTENT_TYPE, ""): - result["json"] = await request.json() + base_result["json"] = await request.json() else: - result["data"] = await request.post() + base_result["data"] = await request.post() - result["query"] = request.query - result["description"] = "webhook" - result.update(**trigger_data) - hass.async_run_hass_job(job, {"trigger": result}) + base_result["query"] = request.query + base_result["description"] = "webhook" + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} + ) + for trigger in triggers[webhook_id]: + result = {**base_result, **trigger.automation_info["trigger_data"]} + hass.async_run_hass_job(trigger.job, {"trigger": result}) async def async_attach_trigger( @@ -49,20 +66,32 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" - trigger_data = automation_info["trigger_data"] webhook_id: str = config[CONF_WEBHOOK_ID] job = HassJob(action) - async_register( - hass, - automation_info["domain"], - automation_info["name"], - webhook_id, - partial(_handle_webhook, job, trigger_data), + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} ) + if webhook_id not in triggers: + async_register( + hass, + automation_info["domain"], + automation_info["name"], + webhook_id, + _handle_webhook, + ) + triggers[webhook_id] = [] + + trigger_instance = TriggerInstance(automation_info, job) + triggers[webhook_id].append(trigger_instance) + @callback def unregister(): """Unregister webhook.""" - async_unregister(hass, webhook_id) + triggers[webhook_id].remove(trigger_instance) + if not triggers[webhook_id]: + async_unregister(hass, webhook_id) + triggers.pop(webhook_id) return unregister diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 0bc237b1c11..b7b95dff392 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -840,7 +840,7 @@ async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_clien @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("tag_scanned", store_event) diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 2deac022b1e..e8d88845f5a 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -23,7 +23,7 @@ async def test_webhook_json(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -62,7 +62,7 @@ async def test_webhook_post(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -97,7 +97,7 @@ async def test_webhook_query(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -126,13 +126,68 @@ async def test_webhook_query(hass, hass_client_no_auth): assert events[0].data["hello"] == "yo world" +async def test_webhook_multiple(hass, hass_client_no_auth): + """Test triggering multiple triggers with a POST webhook.""" + events1 = [] + events2 = [] + + @callback + def store_event1(event): + """Help store events.""" + events1.append(event) + + @callback + def store_event2(event): + """Help store events.""" + events2.append(event) + + hass.bus.async_listen("test_success1", store_event1) + hass.bus.async_listen("test_success2", store_event2) + + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success1", + "event_data_template": {"hello": "yo {{ trigger.data.hello }}"}, + }, + }, + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success2", + "event_data_template": { + "hello": "yo2 {{ trigger.data.hello }}" + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + client = await hass_client_no_auth() + + await client.post("/api/webhook/post_webhook", data={"hello": "world"}) + await hass.async_block_till_done() + + assert len(events1) == 1 + assert events1[0].data["hello"] == "yo world" + assert len(events2) == 1 + assert events2[0].data["hello"] == "yo2 world" + + async def test_webhook_reload(hass, hass_client_no_auth): """Test reloading a webhook.""" events = [] @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) From 7140a9d025496488d5feae17b3dc7f356a5abb4c Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Aug 2022 02:56:50 -0400 Subject: [PATCH 792/820] Bump AIOAladdinConnect to 0.1.37 (#76046) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index a142e838f3e..008b8f81c89 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.33"], + "requirements": ["AIOAladdinConnect==0.1.37"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 4d3e28cce46..bd55f88f2df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.33 +AIOAladdinConnect==0.1.37 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 431483efd32..31e5e646ad8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.33 +AIOAladdinConnect==0.1.37 # homeassistant.components.adax Adax-local==0.1.4 From 23488f392b0a7a4071051eed40020d2e9f6ecb12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:46:22 -1000 Subject: [PATCH 793/820] Lower bluetooth startup timeout to 9s to avoid warning (#76050) --- homeassistant/components/bluetooth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 9adaac84333..ba079a426fe 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -46,7 +46,7 @@ _LOGGER = logging.getLogger(__name__) UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 -START_TIMEOUT = 15 +START_TIMEOUT = 9 SOURCE_LOCAL: Final = "local" From 66afd1e696e42853653b96e5ef86b3bfa1651ddf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:36:27 -1000 Subject: [PATCH 794/820] Bump bluetooth-adapters to 0.1.3 (#76052) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f215e8fa161..40e63ec7180 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.2"], + "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.3"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c9d424daa33..bea9b749445 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.0 -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index bd55f88f2df..229a8ad90ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31e5e646ad8..b29ccb60cff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 # homeassistant.components.bond bond-async==0.1.22 From 30cd087f6f0e36f1fcfe13780ae28625ac609921 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:34:48 -1000 Subject: [PATCH 795/820] Fix govee H5074 data (#76057) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index c7909d3e1af..624a38ebe9d 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -24,7 +24,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.5"], + "requirements": ["govee-ble==0.12.6"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 229a8ad90ec..2b38d1155e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.5 +govee-ble==0.12.6 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b29ccb60cff..7ef74a1c6dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.5 +govee-ble==0.12.6 # homeassistant.components.gree greeclimate==1.2.0 From da00f5ba1e019867d9ba17cba1baf6955a9cbdfe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 09:38:21 +0200 Subject: [PATCH 796/820] Bumped version to 2022.8.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9ef45865665..385db6904e4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 284585734dc..fff0c129cec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b4" +version = "2022.8.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b9ee81dfc33704577d13305665eedf2184073f7f Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Tue, 2 Aug 2022 19:22:29 +0200 Subject: [PATCH 797/820] Fix capitalization in mitemp_bt strings (#76063) Co-authored-by: Franck Nijhof --- homeassistant/components/mitemp_bt/strings.json | 2 +- homeassistant/components/mitemp_bt/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json index d36c25eafec..1f9f031a3bb 100644 --- a/homeassistant/components/mitemp_bt/strings.json +++ b/homeassistant/components/mitemp_bt/strings.json @@ -2,7 +2,7 @@ "issues": { "replaced": { "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/mitemp_bt/translations/en.json b/homeassistant/components/mitemp_bt/translations/en.json index cd5113ee02f..78ec041405b 100644 --- a/homeassistant/components/mitemp_bt/translations/en.json +++ b/homeassistant/components/mitemp_bt/translations/en.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced" } } From ed579515712ced262c88af69eeb9df031df5bdb1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 11:42:57 +0200 Subject: [PATCH 798/820] Small title adjustment to the Home Assistant Alerts integration (#76070) --- homeassistant/components/homeassistant_alerts/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json index 0d276c6f3ae..7c9ddf4f905 100644 --- a/homeassistant/components/homeassistant_alerts/manifest.json +++ b/homeassistant/components/homeassistant_alerts/manifest.json @@ -1,6 +1,6 @@ { "domain": "homeassistant_alerts", - "name": "Home Assistant alerts", + "name": "Home Assistant Alerts", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts", "codeowners": ["@home-assistant/core"], From 676664022d5716b2347044e2d42112412e31136c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 14:13:07 +0200 Subject: [PATCH 799/820] Handle missing attributes in meater objects (#76072) --- homeassistant/components/meater/sensor.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 84ef3a2e2a9..a2753a42307 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -43,14 +43,18 @@ class MeaterSensorEntityDescription( def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert elapsed time to timestamp.""" - if not probe.cook: + if not probe.cook or not hasattr(probe.cook, "time_elapsed"): return None return dt_util.utcnow() - timedelta(seconds=probe.cook.time_elapsed) def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert remaining time to timestamp.""" - if not probe.cook or probe.cook.time_remaining < 0: + if ( + not probe.cook + or not hasattr(probe.cook, "time_remaining") + or probe.cook.time_remaining < 0 + ): return None return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining) @@ -99,7 +103,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.target_temperature if probe.cook else None, + value=lambda probe: probe.cook.target_temperature + if probe.cook and hasattr(probe.cook, "target_temperature") + else None, ), # Peak temperature MeaterSensorEntityDescription( @@ -109,7 +115,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.peak_temperature if probe.cook else None, + value=lambda probe: probe.cook.peak_temperature + if probe.cook and hasattr(probe.cook, "peak_temperature") + else None, ), # Remaining time in seconds. When unknown/calculating default is used. Default: -1 # Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time. From 654e26052bd8c94cef527ca85e36db500701524b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 13:03:34 +0200 Subject: [PATCH 800/820] Remove Somfy from Overkiz title in manifest (#76073) --- homeassistant/components/overkiz/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index a7595065224..6e6e57f12e5 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -1,6 +1,6 @@ { "domain": "overkiz", - "name": "Overkiz (by Somfy)", + "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", "requirements": ["pyoverkiz==1.4.2"], From 2eddbf2381613b39db88c5c6063cf8e0b4064904 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 13:11:06 +0100 Subject: [PATCH 801/820] Fix typo in new xiaomi_ble string (#76076) --- homeassistant/components/xiaomi_ble/strings.json | 2 +- homeassistant/components/xiaomi_ble/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 48d5c3a87f7..9d2a0ae40d8 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -12,7 +12,7 @@ "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "get_encryption_key_legacy": { "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 836ccc51637..4648b28cc93 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -27,7 +27,7 @@ "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "user": { "data": { From c90a223cb6e9f06782eca0ee4abeca67137376a3 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Aug 2022 10:10:20 -0400 Subject: [PATCH 802/820] Bump AIOAladdinConnect to 0.1.39 (#76082) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 008b8f81c89..5e55f391aa6 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.37"], + "requirements": ["AIOAladdinConnect==0.1.39"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 2b38d1155e7..52b514d7b0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.37 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ef74a1c6dd..fd332772724 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.37 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 From cc9a130f580fe25422ede49a5e36d647484cf763 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 19:05:09 +0200 Subject: [PATCH 803/820] Refresh homeassistant_alerts when hass has started (#76083) --- homeassistant/components/homeassistant_alerts/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index d405b9e257d..60386e3d080 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -14,6 +14,7 @@ from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import __version__ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.yaml import parse_yaml @@ -100,7 +101,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: coordinator = AlertUpdateCoordinator(hass) coordinator.async_add_listener(async_schedule_update_alerts) - await coordinator.async_refresh() + + async def initial_refresh(hass: HomeAssistant) -> None: + await coordinator.async_refresh() + + async_at_start(hass, initial_refresh) return True From c4906414ea807502ae2cf5c5a89e813fd77478f1 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 2 Aug 2022 11:29:32 -0400 Subject: [PATCH 804/820] Ensure ZHA devices load before validating device triggers (#76084) --- homeassistant/components/zha/__init__.py | 5 ++++- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/device_trigger.py | 9 +++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 70b9dfd9b46..0a7d43120f7 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -35,6 +35,7 @@ from .core.const import ( DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, + ZHA_DEVICES_LOADED_EVENT, RadioType, ) from .core.discovery import GROUP_PROBE @@ -75,7 +76,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up ZHA from config.""" - hass.data[DATA_ZHA] = {} + hass.data[DATA_ZHA] = {ZHA_DEVICES_LOADED_EVENT: asyncio.Event()} if DOMAIN in config: conf = config[DOMAIN] @@ -109,6 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].set() device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -141,6 +143,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload ZHA config entry.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] await zha_gateway.shutdown() + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].clear() GROUP_PROBE.cleanup() api.async_unload_api(hass) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 4c8c6e03c79..dfa5f608cfe 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -394,6 +394,7 @@ ZHA_GW_MSG_GROUP_REMOVED = "group_removed" ZHA_GW_MSG_LOG_ENTRY = "log_entry" ZHA_GW_MSG_LOG_OUTPUT = "log_output" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" +ZHA_DEVICES_LOADED_EVENT = "zha_devices_loaded_event" EFFECT_BLINK = 0x00 EFFECT_BREATHE = 0x01 diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index cdd98110f83..4ad8eccea1d 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -16,8 +16,8 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError, IntegrationError from homeassistant.helpers.typing import ConfigType -from . import DOMAIN -from .core.const import ZHA_EVENT +from . import DOMAIN as ZHA_DOMAIN +from .core.const import DATA_ZHA, ZHA_DEVICES_LOADED_EVENT, ZHA_EVENT from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" @@ -35,7 +35,8 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) - if "zha" in hass.config.components: + if ZHA_DOMAIN in hass.config.components: + await hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].wait() trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) @@ -100,7 +101,7 @@ async def async_get_triggers( triggers.append( { CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, + CONF_DOMAIN: ZHA_DOMAIN, CONF_PLATFORM: DEVICE, CONF_TYPE: trigger, CONF_SUBTYPE: subtype, From e073f6b4391bc5177ebda1aa9bf6a2f80a42cb4d Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 2 Aug 2022 11:08:33 -0500 Subject: [PATCH 805/820] Bump Frontend to 20220802.0 (#76087) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 45331491aa0..ed9b381ee9d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220728.0"], + "requirements": ["home-assistant-frontend==20220802.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bea9b749445..f2140a9eca7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 52b514d7b0c..71aee1700ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd332772724..61a4b29b3b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 19b0961084068c7e8865ed0db5a20d2f69518a14 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 19:37:52 +0200 Subject: [PATCH 806/820] Bumped version to 2022.8.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 385db6904e4..3e7204e1746 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index fff0c129cec..2fa072c6d30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b5" +version = "2022.8.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 213812f087f78d3997526097cf8e9a73e5822b1a Mon Sep 17 00:00:00 2001 From: Eloston Date: Tue, 2 Aug 2022 02:29:44 +0000 Subject: [PATCH 807/820] Add support for SwitchBot Plug Mini (#76056) --- CODEOWNERS | 4 ++-- homeassistant/components/switchbot/__init__.py | 3 +++ homeassistant/components/switchbot/const.py | 2 ++ homeassistant/components/switchbot/manifest.json | 10 ++++++++-- homeassistant/components/switchbot/sensor.py | 9 ++++++++- homeassistant/components/switchbot/switch.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index bd39fd68590..5853186d0bb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1044,8 +1044,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core -/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas -/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas +/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston +/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode /homeassistant/components/switchmate/ @danielhiversen @qiz-li diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 42ca0856b02..e32252a7615 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -22,6 +22,7 @@ from .const import ( ATTR_CONTACT, ATTR_CURTAIN, ATTR_HYGROMETER, + ATTR_PLUG, CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, @@ -30,6 +31,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], + ATTR_PLUG: [Platform.SWITCH, Platform.SENSOR], ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], @@ -37,6 +39,7 @@ PLATFORMS_BY_TYPE = { CLASS_BY_DEVICE = { ATTR_CURTAIN: switchbot.SwitchbotCurtain, ATTR_BOT: switchbot.Switchbot, + ATTR_PLUG: switchbot.SwitchbotPlugMini, } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index dc5abc139e6..9cc2acebbf8 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -7,12 +7,14 @@ ATTR_BOT = "bot" ATTR_CURTAIN = "curtain" ATTR_HYGROMETER = "hygrometer" ATTR_CONTACT = "contact" +ATTR_PLUG = "plug" DEFAULT_NAME = "Switchbot" SUPPORTED_MODEL_TYPES = { "WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN, "WoSensorTH": ATTR_HYGROMETER, "WoContact": ATTR_CONTACT, + "WoPlug": ATTR_PLUG, } # Config Defaults diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index dcb33c03882..41d0d7efda6 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,10 +2,16 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.16.0"], + "requirements": ["PySwitchbot==0.17.1"], "config_flow": true, "dependencies": ["bluetooth"], - "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], + "codeowners": [ + "@bdraco", + "@danielhiversen", + "@RenierM26", + "@murtas", + "@Eloston" + ], "bluetooth": [ { "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index f796ea05e7b..fb24ae22679 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -33,6 +33,13 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), + "wifi_rssi": SensorEntityDescription( + key="wifi_rssi", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), "battery": SensorEntityDescription( key="battery", native_unit_of_measurement=PERCENTAGE, @@ -98,7 +105,7 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): super().__init__(coordinator, unique_id, address, name=switchbot_name) self._sensor = sensor self._attr_unique_id = f"{unique_id}-{sensor}" - self._attr_name = f"{switchbot_name} {sensor.title()}" + self._attr_name = f"{switchbot_name} {sensor.replace('_', ' ').title()}" self.entity_description = SENSOR_TYPES[sensor] @property diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index e6ba77fa164..65c7588acbd 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -33,7 +33,7 @@ async def async_setup_entry( assert unique_id is not None async_add_entities( [ - SwitchBotBotEntity( + SwitchBotSwitch( coordinator, unique_id, entry.data[CONF_ADDRESS], @@ -44,8 +44,8 @@ async def async_setup_entry( ) -class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): - """Representation of a Switchbot.""" +class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): + """Representation of a Switchbot switch.""" _attr_device_class = SwitchDeviceClass.SWITCH diff --git a/requirements_all.txt b/requirements_all.txt index 71aee1700ab..04fc820bd9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.16.0 +PySwitchbot==0.17.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61a4b29b3b7..077121e7b22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.16.0 +PySwitchbot==0.17.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From c22cb13bd0800e9abcf023b178ea2bf336661b3b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 17:20:37 +0100 Subject: [PATCH 808/820] Add optional context parameter to async_start_reauth (#76077) --- homeassistant/components/xiaomi_ble/sensor.py | 20 +------------- homeassistant/config_entries.py | 7 ++++- tests/test_config_entries.py | 27 +++++++++++++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index b3cd5126967..fef9b334e75 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -181,25 +181,7 @@ def process_service_info( and data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified ): - flow_context = { - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "title_placeholders": {"name": entry.title}, - "unique_id": entry.unique_id, - "device": data, - } - - for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): - if flow["context"] == flow_context: - break - else: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context=flow_context, - data=entry.data, - ) - ) + entry.async_start_reauth(hass, context={"device": data}) return sensor_update_to_bluetooth_data_update(update) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b0c04323005..638aa0b8110 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -640,7 +640,9 @@ class ConfigEntry: await asyncio.gather(*pending) @callback - def async_start_reauth(self, hass: HomeAssistant) -> None: + def async_start_reauth( + self, hass: HomeAssistant, context: dict[str, Any] | None = None + ) -> None: """Start a reauth flow.""" flow_context = { "source": SOURCE_REAUTH, @@ -649,6 +651,9 @@ class ConfigEntry: "unique_id": self.unique_id, } + if context: + flow_context.update(context) + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain): if flow["context"] == flow_context: return diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 3e7245ed73a..66f51508441 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3273,3 +3273,30 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): with pytest.raises(config_entries.OperationNotAllowed): assert await manager.async_reload(entry.entry_id) assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + +async def test_reauth(hass): + """Test the async_reauth_helper.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"} + assert flows[0]["context"]["extra_context"] == "some_extra_context" + + # Check we can't start duplicate flows + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(flows) == 1 From 690f051a87c11d63b9cdc06366f3cd6f8c17cda3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 09:11:50 -1000 Subject: [PATCH 809/820] Bump pyatv to 0.10.3 (#76091) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index dec195fddee..5717f851b81 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.10.2"], + "requirements": ["pyatv==0.10.3"], "dependencies": ["zeroconf"], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 04fc820bd9f..87ad8ffc67e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1392,7 +1392,7 @@ pyatmo==6.2.4 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 077121e7b22..058971731c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -968,7 +968,7 @@ pyatag==0.3.5.3 pyatmo==6.2.4 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From a78da6a000a0f37f3921f1107b367bbf605d9300 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 21:38:38 +0100 Subject: [PATCH 810/820] Fix serialization of Xiaomi BLE reauth flow (#76095) * Use data instead of context to fix serialisation bug * Test change to async_start_reauth --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 2 +- homeassistant/config_entries.py | 7 +++++-- tests/components/xiaomi_ble/test_config_flow.py | 3 +-- tests/test_config_entries.py | 12 ++++++++++-- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 092c60e9713..725c513914f 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -260,7 +260,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None - device: DeviceData = self.context["device"] + device: DeviceData = entry_data["device"] self._discovered_device = device self._discovery_info = device.last_service_info diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index fef9b334e75..dcb95422609 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -181,7 +181,7 @@ def process_service_info( and data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified ): - entry.async_start_reauth(hass, context={"device": data}) + entry.async_start_reauth(hass, data={"device": data}) return sensor_update_to_bluetooth_data_update(update) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 638aa0b8110..7c2f1c84ff9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -641,7 +641,10 @@ class ConfigEntry: @callback def async_start_reauth( - self, hass: HomeAssistant, context: dict[str, Any] | None = None + self, + hass: HomeAssistant, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" flow_context = { @@ -662,7 +665,7 @@ class ConfigEntry: hass.config_entries.flow.async_init( self.domain, context=flow_context, - data=self.data, + data=self.data | (data or {}), ) ) diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 0d123f0cd54..32ba6be3322 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1033,9 +1033,8 @@ async def test_async_step_reauth_abort_early(hass): "entry_id": entry.entry_id, "title_placeholders": {"name": entry.title}, "unique_id": entry.unique_id, - "device": device, }, - data=entry.data, + data=entry.data | {"device": device}, ) assert result["type"] == FlowResultType.ABORT diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 66f51508441..b923e37b636 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3286,8 +3286,14 @@ async def test_reauth(hass): await entry.async_setup(hass) await hass.async_block_till_done() - entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) - await hass.async_block_till_done() + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init) as mock_init: + entry.async_start_reauth( + hass, + context={"extra_context": "some_extra_context"}, + data={"extra_data": 1234}, + ) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -3296,6 +3302,8 @@ async def test_reauth(hass): assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"} assert flows[0]["context"]["extra_context"] == "some_extra_context" + assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234 + # Check we can't start duplicate flows entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() From d7a418a219b1732c43c60165fcecc5b93a35247a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Aug 2022 17:54:06 +0200 Subject: [PATCH 811/820] Guard imports for type hinting in Bluetooth (#75984) --- homeassistant/components/bluetooth/__init__.py | 12 ++++++++---- homeassistant/components/bluetooth/config_flow.py | 6 ++++-- homeassistant/components/bluetooth/match.py | 12 ++++++++---- homeassistant/components/bluetooth/models.py | 7 +++++-- .../bluetooth/passive_update_coordinator.py | 11 +++++++---- .../components/bluetooth/passive_update_processor.py | 12 ++++++++---- 6 files changed, 40 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index ba079a426fe..39629ab6d85 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,12 +8,10 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -from typing import Final +from typing import TYPE_CHECKING, Final import async_timeout from bleak import BleakError -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -27,7 +25,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo -from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_bluetooth from . import models @@ -42,6 +39,13 @@ from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .util import async_get_bluetooth_adapters +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + from homeassistant.helpers.typing import ConfigType + + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index bbba5f411b2..1a0be8706bf 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,18 +1,20 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol from homeassistant.components import onboarding from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN from .util import async_get_bluetooth_adapters +if TYPE_CHECKING: + from homeassistant.data_entry_flow import FlowResult + class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Bluetooth.""" diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 000f39eefd4..2cd4f62ae5e 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -1,17 +1,21 @@ """The bluetooth integration matchers.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass import fnmatch -from typing import Final, TypedDict +from typing import TYPE_CHECKING, Final, TypedDict -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from lru import LRU # pylint: disable=no-name-in-module from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional +if TYPE_CHECKING: + from collections.abc import Mapping + + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + MAX_REMEMBER_ADDRESSES: Final = 2048 diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 6f814c7b66b..51704a2f530 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -4,10 +4,9 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import Any, Final +from typing import TYPE_CHECKING, Any, Final from bleak import BleakScanner -from bleak.backends.device import BLEDevice from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, @@ -16,6 +15,10 @@ from bleak.backends.scanner import ( from homeassistant.core import CALLBACK_TYPE, callback as hass_callback +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + _LOGGER = logging.getLogger(__name__) FILTER_UUIDS: Final = "UUIDs" diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 31a6b065830..5c6b5b79509 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,16 +1,19 @@ """Passive update coordinator for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Generator -import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .update_coordinator import BasePassiveBluetoothCoordinator +if TYPE_CHECKING: + from collections.abc import Callable, Generator + import logging + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): """Class to manage passive bluetooth advertisements. diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 1f2047c02cb..78966d9b7ab 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -1,20 +1,24 @@ """Passive update processors for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Mapping import dataclasses import logging -from typing import Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator +if TYPE_CHECKING: + from collections.abc import Callable, Mapping + + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + @dataclasses.dataclass(frozen=True) class PassiveBluetoothEntityKey: From d2dc83c4c7fc9b7375a7f7adc72d9ccab7347f62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 13:46:43 -1000 Subject: [PATCH 812/820] Handle additional bluetooth start exceptions (#76096) --- .../components/bluetooth/__init__.py | 33 +++++- tests/components/bluetooth/test_init.py | 110 ++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 39629ab6d85..c91563d7729 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Final import async_timeout from bleak import BleakError +from dbus_next import InvalidMessageError from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -26,6 +27,7 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.loader import async_get_bluetooth +from homeassistant.util.package import is_docker_env from . import models from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN @@ -341,13 +343,42 @@ class BluetoothManager: try: async with async_timeout.timeout(START_TIMEOUT): await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + self._cancel_device_detected() + _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) + raise ConfigEntryNotReady( + f"Invalid DBus message received: {ex}; try restarting `dbus`" + ) from ex + except BrokenPipeError as ex: + self._cancel_device_detected() + _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + self._cancel_device_detected() + _LOGGER.debug( + "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ConfigEntryNotReady( + f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" + ) from ex except asyncio.TimeoutError as ex: self._cancel_device_detected() raise ConfigEntryNotReady( f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" ) from ex - except (FileNotFoundError, BleakError) as ex: + except BleakError as ex: self._cancel_device_detected() + _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a47916506df..edc5eb024a6 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice +from dbus_next import InvalidMessageError import pytest from homeassistant.components import bluetooth @@ -1409,3 +1410,112 @@ async def test_changing_the_adapter_at_runtime(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() + + +async def test_dbus_socket_missing_in_container(hass, caplog): + """Test we handle dbus being missing in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "/run/dbus" in caplog.text + assert "docker" in caplog.text + + +async def test_dbus_socket_missing(hass, caplog): + """Test we handle dbus being missing.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "docker" not in caplog.text + + +async def test_dbus_broken_pipe_in_container(hass, caplog): + """Test we handle dbus broken pipe in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text + assert "restarting" in caplog.text + assert "container" in caplog.text + + +async def test_dbus_broken_pipe(hass, caplog): + """Test we handle dbus broken pipe.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "restarting" in caplog.text + assert "container" not in caplog.text + + +async def test_invalid_dbus_message(hass, caplog): + """Test we handle invalid dbus message.""" + + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=InvalidMessageError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text From 51a6899a60c6944ec9b66f778cd9eb7a7065cdee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 10:38:01 -1000 Subject: [PATCH 813/820] Only stat the .dockerenv file once (#76097) --- homeassistant/util/package.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 18ab43967ec..49ab3c10f8c 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from functools import cache from importlib.metadata import PackageNotFoundError, version import logging import os @@ -23,6 +24,7 @@ def is_virtual_env() -> bool: ) +@cache def is_docker_env() -> bool: """Return True if we run in a docker env.""" return Path("/.dockerenv").exists() From ad14b5f3d7db2cf6d5debff8a295516eae8938b4 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 22:05:36 +0100 Subject: [PATCH 814/820] Fix Xiaomi BLE UI string issues (#76099) --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- homeassistant/components/xiaomi_ble/strings.json | 12 +++++++----- .../components/xiaomi_ble/translations/en.json | 14 ++++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 725c513914f..a05e703db6a 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -189,7 +189,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Ack that device is slow.""" - if user_input is not None or not onboarding.async_is_onboarded(self.hass): + if user_input is not None: return self._async_get_or_create_entry() self._set_confirm_only() diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 9d2a0ae40d8..5ecbb8e1b88 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -11,7 +11,7 @@ "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, - "slow_confirm": { + "confirm_slow": { "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "get_encryption_key_legacy": { @@ -27,14 +27,16 @@ } } }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 4648b28cc93..2cb77dd2c07 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -3,17 +3,22 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful" }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "confirm_slow": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -26,9 +31,6 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, - "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." - }, "user": { "data": { "address": "Device" From d85129c52753a152c0af26fd7eaf5eb61c7deefd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 20:35:41 -1000 Subject: [PATCH 815/820] Bump aiohomekit to 1.2.3 to fix hang at startup (#76102) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 2de2a915d41..ac1be576906 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.2"], + "requirements": ["aiohomekit==1.2.3"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 87ad8ffc67e..5e277e8e801 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.2 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 058971731c1..acf67deb250 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.2 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http From 42a1f6ca20794f56b1b3afe09702e5ef884cf09f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 20:34:46 -1000 Subject: [PATCH 816/820] Bump pySwitchbot to 0.17.3 to fix hang at startup (#76103) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 41d0d7efda6..f01eae4a938 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.17.1"], + "requirements": ["PySwitchbot==0.17.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5e277e8e801..78d5a3db456 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index acf67deb250..01ba627194f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From bc1e371cae25cad0491dd78879f51229dd474af2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Aug 2022 08:58:12 +0200 Subject: [PATCH 817/820] Bumped version to 2022.8.0b7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3e7204e1746..dd5a41a7aa5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 2fa072c6d30..8a65560b44a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b6" +version = "2022.8.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 29f6d7818a09f60f5abd0a24a935d5fa06b214e2 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Wed, 3 Aug 2022 10:31:09 +0200 Subject: [PATCH 818/820] Bump `azure-servicebus` to support py3.10 (#76092) Bump azure-servicebus --- .../azure_service_bus/manifest.json | 2 +- .../components/azure_service_bus/notify.py | 24 ++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json index 6cf5e2bf406..26ccea446f2 100644 --- a/homeassistant/components/azure_service_bus/manifest.json +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -2,7 +2,7 @@ "domain": "azure_service_bus", "name": "Azure Service Bus", "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", - "requirements": ["azure-servicebus==0.50.3"], + "requirements": ["azure-servicebus==7.8.0"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_push", "loggers": ["azure"] diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 0d48ff6b2d6..53873373011 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -2,11 +2,12 @@ import json import logging -from azure.servicebus.aio import Message, ServiceBusClient -from azure.servicebus.common.errors import ( - MessageSendFailed, +from azure.servicebus import ServiceBusMessage +from azure.servicebus.aio import ServiceBusClient +from azure.servicebus.exceptions import ( + MessagingEntityNotFoundError, ServiceBusConnectionError, - ServiceBusResourceNotFound, + ServiceBusError, ) import voluptuous as vol @@ -60,10 +61,10 @@ def get_service(hass, config, discovery_info=None): try: if queue_name: - client = servicebus.get_queue(queue_name) + client = servicebus.get_queue_sender(queue_name) else: - client = servicebus.get_topic(topic_name) - except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err: + client = servicebus.get_topic_sender(topic_name) + except (ServiceBusConnectionError, MessagingEntityNotFoundError) as err: _LOGGER.error( "Connection error while creating client for queue/topic '%s'. %s", queue_name or topic_name, @@ -93,11 +94,12 @@ class ServiceBusNotificationService(BaseNotificationService): if data := kwargs.get(ATTR_DATA): dto.update(data) - queue_message = Message(json.dumps(dto)) - queue_message.properties.content_type = CONTENT_TYPE_JSON + queue_message = ServiceBusMessage( + json.dumps(dto), content_type=CONTENT_TYPE_JSON + ) try: - await self._client.send(queue_message) - except MessageSendFailed as err: + await self._client.send_messages(queue_message) + except ServiceBusError as err: _LOGGER.error( "Could not send service bus notification to %s. %s", self._client.name, diff --git a/requirements_all.txt b/requirements_all.txt index 78d5a3db456..5027b7df0cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -375,7 +375,7 @@ axis==44 azure-eventhub==5.7.0 # homeassistant.components.azure_service_bus -azure-servicebus==0.50.3 +azure-servicebus==7.8.0 # homeassistant.components.baidu baidu-aip==1.6.6 From 81ee24738bbafccae01cf25c84060cb2da622ba1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 3 Aug 2022 09:41:00 +0200 Subject: [PATCH 819/820] Fix deconz group log warning (#76114) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 8019d0df2df..6384ebfcd5f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==100"], + "requirements": ["pydeconz==101"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 5027b7df0cd..6e1850d33a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1455,7 +1455,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==100 +pydeconz==101 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01ba627194f..6f75b540b28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1001,7 +1001,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==100 +pydeconz==101 # homeassistant.components.dexcom pydexcom==0.2.3 From 80a053a4cde0ec75c532f3a4870c742561579994 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Aug 2022 11:42:18 +0200 Subject: [PATCH 820/820] Bumped version to 2022.8.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dd5a41a7aa5..18561a8bd2e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 8a65560b44a..c7e187e07f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b7" +version = "2022.8.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"